用 C++ 写编译器踩坑后,我选择了 Rust

从 C++ 迁移到 Rust 编写编译器的真实经历:我遇到了哪些坑,Rust 如何帮我解决的

前言

这篇文章记录了我用 C++ 和 Rust 分别实现编译器的真实经历。不是语言圣战,只是分享一些踩坑经历。 经历是真实的, 但是采用了大量 AI 辅助

背景

我先用 C++ 写了一个计算器编译器,能用,但踩了不少坑。后来听说 Rust 适合写编译器,就用 Rust 重写了一遍。

结果:两个版本都能跑,Rust 版本开发体验更舒服一些,但 C++ 版本也没啥大问题。

声明

这不是"Rust 吊打 C++"的文章

  • C++ 能做的,Rust 能做
  • Rust 能做的,C++ 也能做(可能麻烦点)
  • 选哪个不重要,完成项目才重要

我只是分享真实经历,你可以参考,但别盲从。最后会聊聊"工具选择"这个话题。

适合阅读的人

  • 正在纠结用什么语言写编译器
  • 想了解 Rust 和 C++ 在实际项目中的差异
  • 对编译器开发感兴趣

不适合的人

  • 想看"X 语言完胜 Y 语言"的结论
  • 期待看性能跑分
  • 只想找人给你做选择(我不会告诉你"必须选 Rust")

为什么先用 C++?

一开始选 C++ 很自然:

  • 大学学的就是 C++
  • 《编译原理》课本里的例子都是 C++
  • 网上大部分编译器教程也用 C++
  • LLVM、GCC 这些成熟项目都用 C++

于是我就开始写了。然后就开始踩坑了...

第一个坑:AST 定义太繁琐

C++ 版本:继承地狱

一开始我用面向对象的方式定义 AST:

rust
#[derive(Debug, Clone, PartialEq)]
pub enum Node {
    Add(Box<Node>, Box<Node>),
    Subtract(Box<Node>, Box<Node>),
    Multiply(Box<Node>, Box<Node>),
    Divide(Box<Node>, Box<Node>),
    Power(Box<Node>, Box<Node>),
    Negative(Box<Node>),
    Number(Decimal),
}

impl Node {
    pub fn evaluate(&self) -> Decimal {
        match self {
            Node::Add(left, right) => left.evaluate() + right.evaluate(),
            Node::Subtract(left, right) => left.evaluate() - right.evaluate(),
            Node::Multiply(left, right) => left.evaluate() * right.evaluate(),
            Node::Divide(left, right) => left.evaluate() / right.evaluate(),
            Node::Power(left, right) => left.evaluate().powd(right.evaluate()),
            Node::Negative(node) => -node.evaluate(),
            Node::Number(n) => *n,
        }
    }
}

这种写法的好处很明显:

  • 类型安全:编译器强制你处理所有可能的节点类型
  • 内存高效:枚举的内存布局是优化过的
  • 可读性强:一眼就能看出所有可能的 AST 节点类型
cpp
// 基类定义
struct ASTNode {
    virtual ~ASTNode() = default;
    virtual void accept(ASTVisitor& v) = 0;
    Position pos;
};

// 各种具体的节点类型
struct AddExp : public ASTNode {
    std::unique_ptr<Exp> left;
    std::unique_ptr<Exp> right;
    void accept(ASTVisitor& v) override { v.visit(*this); }
};

struct MulExp : public ASTNode {
    std::unique_ptr<Exp> left;
    std::unique_ptr<Exp> right;
    void accept(ASTVisitor& v) override { v.visit(*this); }
};

struct Number : public ASTNode {
    int64_t value;
    void accept(ASTVisitor& v) override { v.visit(*this); }
};

// 访问者模式的实现
class ASTVisitor {
public:
    virtual void visit(AddExp& node) = 0;
    virtual void visit(MulExp& node) = 0;
    virtual void visit(Number& node) = 0;
    // ... 更多的 visit 方法
};

问题来了

  1. 每添加一个新的 AST 节点类型,要改好几个地方
  2. 访问者模式的样板代码太多
  3. 忘记实现某个 visit 方法?编译器不会提醒你
  4. 虚函数调用有性能损耗(虽然不大)

尝试 std::variant:走进模板深渊

听说 C++17 有 std::variant,试了一下:

cpp
#include <variant>
#include <memory>

struct Number { double value; };
struct Add;
struct Multiply;

using NodePtr = std::unique_ptr<std::variant<Number, Add, Multiply>>;

struct Add {
    NodePtr left;
    NodePtr right;
};

struct Multiply {
    NodePtr left;
    NodePtr right;
};

using Node = std::variant<Number, Add, Multiply>;

// 访问需要用 std::visit
double evaluate(const Node& node) {
    return std::visit([](const auto& n) -> double {
        if constexpr (std::is_same_v<std::decay_t<decltype(n)>, Number>) {
            return n.value;
        } else if constexpr (std::is_same_v<std::decay_t<decltype(n)>, Add>) {
            return evaluate(*n.left) + evaluate(*n.right);
        } else if constexpr (std::is_same_v<std::decay_t<decltype(n)>, Multiply>) {
            return evaluate(*n.left) * evaluate(*n.right);
        }
    }, node);
}

结果更糟了

  • 模板错误信息根本看不懂
  • 递归定义要绕弯子
  • if constexpr + std::is_same_v 的写法太丑了
  • 编译时间变长了

转向 Rust:豁然开朗

然后我用 Rust 重写了这部分,代码量直接减半:

rust
#[derive(Debug, Clone, PartialEq)]
pub enum Node {
    Add(Box<Node>, Box<Node>),
    Subtract(Box<Node>, Box<Node>),
    Multiply(Box<Node>, Box<Node>),
    Divide(Box<Node>, Box<Node>),
    Power(Box<Node>, Box<Node>),
    Negative(Box<Node>),
    Number(Decimal),
}

impl Node {
    pub fn evaluate(&self) -> Decimal {
        match self {
            Node::Add(left, right) => left.evaluate() + right.evaluate(),
            Node::Subtract(left, right) => left.evaluate() - right.evaluate(),
            Node::Multiply(left, right) => left.evaluate() * right.evaluate(),
            Node::Divide(left, right) => left.evaluate() / right.evaluate(),
            Node::Power(left, right) => left.evaluate().powd(right.evaluate()),
            Node::Negative(node) => -node.evaluate(),
            Node::Number(n) => *n,
        }
    }
}

感受

  • ✅ 所有节点类型一目了然
  • match 强制处理所有分支,遗漏了编译器会报错
  • ✅ 添加新节点类型,编译器会告诉你哪里需要修改
  • ✅ 没有虚函数,没有访问者模式,简洁明了

第二个坑:词法分析中的分支遗漏

C++ 的 switch 陷阱

写词法分析器时,要处理各种不同的字符。C++ 的 switch 看起来挺好:

rust
impl<'a> Lexer<'a> {
    pub fn tokenize(&mut self) -> Vec<Token> {
        let mut tokens = Vec::new();
        
        loop {
            self.skip_whitespace();
            let pos = self.current_position();
            let c = self.peek_char();
            
            if c.is_none() {
                break; // End of file
            }
            
            let token = match c.unwrap() {
                // 字母或下划线开头 -> 标识符或关键字
                c if c.is_ascii_alphabetic() || c == '_' => 
                    self.read_identifier_or_keyword(pos),
                // 数字开头 -> 数值字面量  
                c if c.is_ascii_digit() => 
                    self.read_number(pos),
                // 字符串字面量
                '"' => self.read_string_literal(pos),
                '\'' => self.read_char_literal(pos),
                // 操作符处理,支持双字符操作符
                '=' => self.read_operator_token(pos, '=', 
                    TokenType::AssignTok, TokenType::EqualTok),
                '&' => self.read_operator_token(pos, '&', 
                    TokenType::AndTok, TokenType::AndAndTok),
                '|' => self.read_operator_token(pos, '|', 
                    TokenType::OrTok, TokenType::OrOrTok),
                '!' => self.read_operator_token(pos, '=', 
                    TokenType::BangTok, TokenType::NotEqualTok),
                // 其他单字符 token
                _ => self.read_single_char_token(pos),
            };
            
            tokens.push(token);
        }
        
        tokens
    }
}

Match Guard 的强大功能

Rust 的 match guard 让条件判断更加清晰:

rust
// 处理不同类型的 Token,包含复杂的条件判断
match token.token_type {
    TokenType::IdentTok if is_keyword(&token.literal) => {
        // 处理关键字
        convert_to_keyword_token(token)
    },
    TokenType::IntLitTok if token.literal.len() > 10 => {
        // 处理可能溢出的大整数
        handle_big_integer(token)
    },
    TokenType::StringLitTok if token.literal.contains('\\') => {
        // 处理包含转义字符的字符串
        process_escape_sequences(token)
    },
    _ => token,
}
cpp
void Lexer::tokenize() {
    while (true) {
        skip_whitespace();
        auto c = input_.peek();
        if (!c) break;
        
        switch (*c) {
            case '"':
                read_string();
                break;
            case '0'...'9':  // 这个语法 C++ 不支持!
                read_number();
                break;
            default:
                if (std::isalpha(*c) || *c == '_') {
                    read_identifier();
                } else {
                    // 处理其他情况...
                }
        }
    }
}

真实踩的坑

  • 某次我添加了新的 Token 类型(比如 @ 符号),忘记在某个 switch 里处理
  • 编译器没有任何警告
  • 运行时解析到 @ 就报错了
  • 花了半小时才找到是哪里遗漏了

Rust 的穷尽性检查救了我

用 Rust 重写后,这类问题消失了:

rust
impl<'a> Lexer<'a> {
    pub fn tokenize(&mut self) -> Vec<Token> {
        let mut tokens = Vec::new();
        
        loop {
            self.skip_whitespace();
            let pos = self.current_position();
            let c = self.peek_char();
            
            if c.is_none() {
                break;
            }
            
            let token = match c.unwrap() {
                c if c.is_ascii_alphabetic() || c == '_' => 
                    self.read_identifier_or_keyword(pos),
                c if c.is_ascii_digit() => 
                    self.read_number(pos),
                '"' => self.read_string_literal(pos),
                '\'' => self.read_char_literal(pos),
                '=' => self.read_operator_token(pos, '=', 
                    TokenType::AssignTok, TokenType::EqualTok),
                '&' => self.read_operator_token(pos, '&', 
                    TokenType::AndTok, TokenType::AndAndTok),
                // ... 其他操作符
                _ => self.read_single_char_token(pos),
            };
            
            tokens.push(token);
        }
        
        tokens
    }
}

后来添加新 Token 类型

rust
// 在枚举中添加新类型
enum TokenType {
    // ... 原有类型
    AtTok,  // 新增 @ 符号
}

编译时立即报错:

error[E0004]: non-exhaustive patterns: `'@'` not covered
  --> src/lexer.rs:45:13
   |
45 |             let token = match c.unwrap() {
   |                         ^^^^^^^^^^^^^^^ pattern `'@'` not covered

这就是穷尽性检查的威力:编译器强制你处理所有情况,不会遗漏。

第三个坑:内存拷贝导致性能问题

C++ 版本:到处都是拷贝

最初的 C++ 实现,解析大文件时内存占用很高:

rust
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct Position {
    pub line: usize,
    pub column: usize,
}

#[derive(Debug, PartialEq, Eq, Clone)]
pub struct Token {
    pub token_type: TokenType,
    pub literal: String,  // 这里还是用的 String,但可以优化为 &str
    pub position: Position,
}

// 更优化的版本,使用生命周期参数
#[derive(Debug, PartialEq, Eq)]
pub struct Token<'a> {
    pub token_type: TokenType,
    pub literal: &'a str,    // 直接引用源码,零拷贝!
    pub position: Position,
}

impl<'a> Lexer<'a> {
    pub fn new(input: &'a str) -> Self {
        Lexer {
            chars: input.chars().peekable(),
            line: 1,
            column: 1,
        }
    }
    
    // 读取标识符时直接返回字符串切片
    fn read_identifier(&mut self, start_pos: Position) -> Token<'a> {
        let start = self.current_offset;
        
        while let Some(&c) = self.chars.peek() {
            if c.is_ascii_alphanumeric() || c == '_' {
                self.chars.next();
                self.column += 1;
            } else {
                break;
            }
        }
        
        let end = self.current_offset;
        Token {
            token_type: TokenType::IdentTok,
            literal: &self.input[start..end],  // 零拷贝!
            position: start_pos,
        }
    }
}

内存优势明显

  • 所有 Token 的 literal 字段都直接指向源码
  • 不需要额外的字符串分配
  • 内存使用量大幅减少,特别是处理大文件时
cpp
class Token {
public:
    TokenType type;
    std::string literal;  // 每个 Token 都拷贝一份字符串
    Position position;
};

Token read_identifier() {
    size_t start = pos_;
    while (pos_ < input_.size() && 
           (std::isalnum(input_[pos_]) || input_[pos_] == '_')) {
        pos_++;
    }
    
    std::string literal = input_.substr(start, pos_ - start);  // 拷贝!
    return Token(TokenType::IDENTIFIER, literal, current_position());
}

测试一个 1MB 的源文件

  • 生成了约 50000 个 Token
  • 内存占用飙到了 3.5MB
  • 大部分内存都花在字符串拷贝上了

尝试 string_view:悬空引用噩梦

听说 C++17 的 string_view 能零拷贝,试了一下:

cpp
class Token {
public:
    TokenType type;
    std::string_view literal;  // 零拷贝!
    Position position;
};

std::vector<Token> tokenize(const std::string& source) {
    Lexer lexer(source);
    return lexer.tokenize();
}

看起来很完美,内存占用降到了 1.2MB。但是...

第二天遇到了段错误

cpp
void process_file(const std::string& filename) {
    std::string code = read_file(filename);
    auto tokens = tokenize(code);  // OK
    
    // ... 一些其他处理
    code.clear();  // 释放内存,优化一下?
    
    // 崩溃!tokens 中的 string_view 变成悬空引用了
    for (const auto& token : tokens) {
        std::cout << token.literal << std::n;  // 段错误
    }
}

问题根源

  • string_view 只是一个"视图",不拥有数据
  • 源字符串销毁后,string_view 就指向了无效内存
  • 编译器完全不会警告你

我花了 2 小时调试这个问题,最后还是得回到 std::string 拷贝...

Rust 的零拷贝 + 编译期安全

用 Rust 重写后,既有零拷贝的性能,又有编译期安全保证:

rust
#[derive(Debug, PartialEq, Eq)]
pub struct Token<'a> {
    pub token_type: TokenType,
    pub literal: &'a str,    // 直接引用源码,零拷贝!
    pub position: Position,
}

fn tokenize<'a>(source: &'a str) -> Vec<Token<'a>> {
    let mut lexer = Lexer::new(source);
    lexer.tokenize()
}

// 尝试写出会崩溃的代码
fn buggy_code() {
    let code = read_file("test.txt");
    let tokens = tokenize(&code);
    
    drop(code);  // 尝试释放 code
    
    // 编译错误!编译器拒绝编译这段代码
    println!("{:?}", tokens[0].literal);
}

编译器会报错

error[E0505]: cannot move out of `code` because it is borrowed
  --> src/main.rs:10:5
   |
8  |     let tokens = tokenize(&code);
   |                           ----- borrow of `code` occurs here
9  |     
10 |     drop(code);
   |     ^^^^^^^^^^ move out of `code` occurs here
11 |     
12 |     println!("{:?}", tokens[0].literal);
   |                      ------ borrow later used here

结果

  • 内存占用从 3.5MB 降到 1.1MB(降低 68%)
  • 性能提升约 15%(减少了内存分配)
  • 最重要的是:不可能写出悬空引用的代码

第四个坑:构建系统噩梦

C++ 的 CMake 配置地狱

这是我的 C++ 项目的 CMakeLists.txt(简化版):

cmake
cmake_minimum_required(VERSION 3.20)
project(Compiler)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

# 找到所有源文件
file(GLOB_RECURSE SOURCES "src/*.cpp")
file(GLOB_RECURSE HEADERS "include/*.hpp")

# 创建可执行文件
add_executable(compiler ${SOURCES})

# 设置头文件路径
target_include_directories(compiler PRIVATE include)

# 编译器警告设置
if(MSVC)
    target_compile_options(compiler PRIVATE /W4)
else()
    target_compile_options(compiler PRIVATE -Wall -Wextra -pedantic)
endif()

# Debug 和 Release 配置
set(CMAKE_CXX_FLAGS_DEBUG "-g -O0")
set(CMAKE_CXX_FLAGS_RELEASE "-O3 -DNDEBUG")

# 如果要添加外部库,比如 fmt
find_package(fmt REQUIRED)
target_link_libraries(compiler fmt::fmt)

# 测试配置
enable_testing()
find_package(GTest REQUIRED)
add_executable(compiler_tests test/test_lexer.cpp test/test_parser.cpp)
target_link_libraries(compiler_tests GTest::gtest_main)
add_test(NAME compiler_tests COMMAND compiler_tests)

这还只是基础配置! 如果要添加更多库,或者支持不同平台,CMakeLists.txt 会变得更加复杂。

包管理的差异

添加依赖库对比

bash
# Rust - 添加一个 JSON 解析库
cargo add serde_json
# 完毕!

# C++ - 添加同样的功能
# 方法1: 用包管理器 (如果你的发行版有的话)
sudo apt install libjsoncpp-dev  # 版本可能过老

# 方法2: 用 vcpkg
git clone https://github.com/Microsoft/vcpkg.git
./vcpkg/bootstrap-vcpkg.sh
./vcpkg/vcpkg install jsoncpp
# 然后修改 CMakeLists.txt...

# 方法3: 用 Conan
pip install conan
echo "jsoncpp/1.9.5" > conanfile.txt
conan install . --build=missing
# 然后修改 CMakeLists.txt...

# 方法4: Git submodule
git submodule add https://github.com/open-source-parsers/jsoncpp.git deps/jsoncpp
# 然后修改 CMakeLists.txt 添加子目录...

Rust 一行命令,C++ 需要选择一种包管理方案并配置构建系统。

IDE 支持对比

功能Rust (rust-analyzer)C++ (clangd/ccls)
代码补全✅ 极其精确✅ 基本可用
跳转定义✅ 很准确✅ 大部分时候准确
查找引用✅ 准确✅ 基本准确
重构支持✅ 安全重命名⚠️ 需要小心
错误提示✅ 清晰⚠️ 模板错误难读
项目配置✅ 零配置⚠️ 需要生成 compile_commands.json

实际体验:Rust 开箱即用,C++ 需要配置一下,但配置好了也挺好用。

性能和编译时间对比:实测数据(随便写了两个版本的计算器)

既然做了两个版本的编译器,当然要测试一下性能差异。

编译时间对比

项目规模

  • Rust 版本:约 1200 行代码,5 个模块
  • C++ 版本:约 1800 行代码(更多样板代码),8 个文件
构建类型Rust (cargo)C++ (cmake + gcc)C++ (cmake + clang)
首次编译2.3s4.1s3.8s
增量编译0.4s1.2s1.1s
清空重构建1.8s3.9s3.6s

测试环境: Intel i5-1340P, 16GB RAM, SSD

运行时性能对比

用同一个测试文件(包含 10000 个 Token 的数学表达式)测试:

阶段Rust 实现C++ 实现差异
词法分析12ms15msRust 快 20%
语法分析28ms31msRust 快 10%
内存使用2.1MB3.4MBRust 省 38%

有趣的发现

  • Rust 的零拷贝策略确实节省了大量内存
  • 得益于更好的编译器优化,Rust 版本运行更快
  • C++ 版本的虚函数调用和动态内存分配是性能瓶颈

二进制文件大小

构建模式RustC++ (gcc)C++ (clang)
Debug8.2MB2.1MB2.3MB
Release1.4MB1.8MB1.6MB

C++ 的 Debug 版本更小,但 Rust 的 Release 版本更小且包含更多调试信息。

How To Choose ?

总结一些实际开发中的问题。

Rust 的学习曲线

rust
// 刚开始经常写出这样的代码,然后被编译器教育
fn parse_expression(&mut self) -> Expression {
    let left = self.parse_primary();
    let operator = self.current_token();  // 借用了 self
    self.advance();  // 错误!self 已经被借用了
    let right = self.parse_primary();
    // ...
}

// 修正版本:明确生命周期
fn parse_expression(&mut self) -> Expression {
    let left = self.parse_primary();
    let operator = *self.current_token();  // 复制而不是借用
    self.advance();  // OK
    let right = self.parse_primary();
    // ...
}

但是,一旦适应了 Rust 的思维方式,这些约束反而帮你写出更安全的代码。C++ 版本我就遇到过几次悬空指针的问题。

C++ 的复杂性管理

C++ 项目到后期变得越来越难管理:

cpp
// 内存管理变得复杂
class Parser {
private:
    std::vector<std::unique_ptr<ASTNode>> nodes_;  // 谁负责释放?
    std::shared_ptr<SymbolTable> symbol_table_;   // 循环引用?
    const Token* current_token_;  // 悬空指针风险
    
public:
    std::unique_ptr<Expression> parse_expression() {
        auto left = parse_primary();
        // 如果这里抛异常,left 的内存会被正确释放吗?
        auto right = parse_primary();
        return std::make_unique<BinaryExpression>(
            std::move(left), std::move(right));
    }
};

虽然现代 C++ 有智能指针,但组合使用时还是容易出错。

调试体验对比

Rust 的错误信息是真的好

rust
error[E0382]: borrow of moved value: `token`
  --> src/parser.rs:45:20
   |
43 |     let first = self.consume_token(token);
   |                                    ----- value moved here
44 |     let second = self.parse_primary();
45 |     self.error_at(token, "Invalid syntax");
   |                    ^^^^^ value used here after move
   |
   = note: consider cloning the value before moving it

C++ 的模板错误太恶心

cpp
error: no matching function for call to 'visit'
/usr/include/c++/11/variant:1924:2: note: candidate: 
template<class _Visitor, class... _Variants> 
constexpr decltype(auto) std::visit(_Visitor&& __visitor, _Variants&&...)
/usr/include/c++/11/variant:1924:2: note:   template argument deduction/substitution failed:
// ... 还有 50 行错误信息

社区和生态对比

方面RustC++
包管理⭐⭐⭐⭐⭐ crates.io 统一⭐⭐ 分散且复杂
文档质量⭐⭐⭐⭐⭐ 统一的文档标准⭐⭐⭐ 参差不齐
社区活跃度⭐⭐⭐⭐ 年轻但活跃⭐⭐⭐⭐⭐ 成熟且庞大
学习资源⭐⭐⭐⭐ 质量高但较少⭐⭐⭐⭐⭐ 丰富但质量不一

所以选哪个?

先说结论:都行,能完成工作就好

看完上面这些对比,你可能觉得我是 Rust 传教士。但实话实说:C++ 也完全能写出好的编译器

我用 C++ 写的那个版本也跑得好好的,虽然开发时踩了些坑,但最终都解决了。

真实情况是这样的

C++ 的"问题"大多数都有解决方案

  • AST 定义繁琐?用 std::variant + C++17
  • 内存管理?用智能指针 + string_view
  • 构建系统?选一个包管理器习惯就好

Rust 的优势在于

  • 很多"正确的做法"是默认的,不需要你主动选择
  • 编译器会强制你写安全的代码
  • 工具链统一,不需要选择困难

我更喜欢 Rust,但不是因为"更好"

而是因为:

  1. 心智负担更低:不用担心悬空引用、不用纠结用哪个包管理器
  2. 开发流程更顺:Cargo 一条命令搞定,IDE 开箱即用
  3. 适合我的思维方式:我喜欢函数式 + 强类型

但这是个人偏好,不是客观优劣。

最重要的事:Get Work Done

这是我从 Linus Torvalds 的一个访谈里学到的态度:

工具是用来完成工作的,不是用来争论的。

选你熟悉的工具,快速完成项目,比花时间争论"哪个语言更好"有意义得多。

我用两个语言都写了一遍编译器,花的时间可能够写三个不同的项目了。这种对比本身就是浪费时间,除非你就是为了学习。

实用建议

如果你要开始一个编译器项目

  1. 已经会 C++?直接用 C++,别纠结

  2. 已经会 Rust?直接用 Rust,别纠结

  3. 两个都不会

    • 想快速出成果 → 学 Python(是的,很多编译器用 Python 写)
    • 想学系统编程 → 随便选一个,重要的是开始写
  4. 最关键的

    • ✅ 先把编译器写出来
    • ❌ 别陷入"完美工具"的陷阱

语言只是工具

  • GCC 用 C++ 写的,很强大
  • Rust 编译器本身用 Rust 写的,也很强大
  • CPython 用 C 写的,还是很强大
  • PyPy 用 Python 写的,依然很强大

关键不是语言,是你对编译器原理的理解。

总结

写完两个版本后,我的真实感受:

  1. Rust 的开发体验确实更好(对我来说)
  2. C++ 也完全能胜任,只是需要更小心
  3. 但这些都不重要,重要的是把项目完成

如果你也想写编译器:

  • 别花时间纠结语言
  • 选一个你熟悉的(或想学的)
  • 然后开始写

工具服务于目标,不要本末倒置。


P.S. 写完这篇文章后我意识到,花时间写这篇"对比文章"本身可能就是在浪费时间。如果你也在纠结语言选择,不如直接开始写代码,边做边学。真正的经验来自实践,不是来自文章。

参考资料

编译器学习

语言特性

关于工具选择的思考

修改于 2025-11-06