前言
这篇文章记录了我用 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:
#[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 节点类型
// 基类定义
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 方法
};问题来了:
- 每添加一个新的 AST 节点类型,要改好几个地方
- 访问者模式的样板代码太多
- 忘记实现某个
visit方法?编译器不会提醒你 - 虚函数调用有性能损耗(虽然不大)
尝试 std::variant:走进模板深渊
听说 C++17 有 std::variant,试了一下:
#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 重写了这部分,代码量直接减半:
#[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 看起来挺好:
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 让条件判断更加清晰:
// 处理不同类型的 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,
}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 重写后,这类问题消失了:
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 类型:
// 在枚举中添加新类型
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++ 实现,解析大文件时内存占用很高:
#[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字段都直接指向源码 - 不需要额外的字符串分配
- 内存使用量大幅减少,特别是处理大文件时
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 能零拷贝,试了一下:
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。但是...
第二天遇到了段错误:
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 重写后,既有零拷贝的性能,又有编译期安全保证:
#[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_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 会变得更加复杂。
包管理的差异
添加依赖库对比:
# 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.3s | 4.1s | 3.8s |
| 增量编译 | 0.4s | 1.2s | 1.1s |
| 清空重构建 | 1.8s | 3.9s | 3.6s |
测试环境: Intel i5-1340P, 16GB RAM, SSD
运行时性能对比
用同一个测试文件(包含 10000 个 Token 的数学表达式)测试:
| 阶段 | Rust 实现 | C++ 实现 | 差异 |
|---|---|---|---|
| 词法分析 | 12ms | 15ms | Rust 快 20% |
| 语法分析 | 28ms | 31ms | Rust 快 10% |
| 内存使用 | 2.1MB | 3.4MB | Rust 省 38% |
有趣的发现:
- Rust 的零拷贝策略确实节省了大量内存
- 得益于更好的编译器优化,Rust 版本运行更快
- C++ 版本的虚函数调用和动态内存分配是性能瓶颈
二进制文件大小
| 构建模式 | Rust | C++ (gcc) | C++ (clang) |
|---|---|---|---|
| Debug | 8.2MB | 2.1MB | 2.3MB |
| Release | 1.4MB | 1.8MB | 1.6MB |
C++ 的 Debug 版本更小,但 Rust 的 Release 版本更小且包含更多调试信息。
How To Choose ?
总结一些实际开发中的问题。
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++ 项目到后期变得越来越难管理:
// 内存管理变得复杂
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 的错误信息是真的好:
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 itC++ 的模板错误太恶心:
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 行错误信息社区和生态对比
| 方面 | Rust | C++ |
|---|---|---|
| 包管理 | ⭐⭐⭐⭐⭐ crates.io 统一 | ⭐⭐ 分散且复杂 |
| 文档质量 | ⭐⭐⭐⭐⭐ 统一的文档标准 | ⭐⭐⭐ 参差不齐 |
| 社区活跃度 | ⭐⭐⭐⭐ 年轻但活跃 | ⭐⭐⭐⭐⭐ 成熟且庞大 |
| 学习资源 | ⭐⭐⭐⭐ 质量高但较少 | ⭐⭐⭐⭐⭐ 丰富但质量不一 |
所以选哪个?
先说结论:都行,能完成工作就好
看完上面这些对比,你可能觉得我是 Rust 传教士。但实话实说:C++ 也完全能写出好的编译器。
我用 C++ 写的那个版本也跑得好好的,虽然开发时踩了些坑,但最终都解决了。
真实情况是这样的
C++ 的"问题"大多数都有解决方案:
- AST 定义繁琐?用
std::variant+ C++17 - 内存管理?用智能指针 +
string_view - 构建系统?选一个包管理器习惯就好
Rust 的优势在于:
- 很多"正确的做法"是默认的,不需要你主动选择
- 编译器会强制你写安全的代码
- 工具链统一,不需要选择困难
我更喜欢 Rust,但不是因为"更好"
而是因为:
- 心智负担更低:不用担心悬空引用、不用纠结用哪个包管理器
- 开发流程更顺:Cargo 一条命令搞定,IDE 开箱即用
- 适合我的思维方式:我喜欢函数式 + 强类型
但这是个人偏好,不是客观优劣。
最重要的事:Get Work Done
这是我从 Linus Torvalds 的一个访谈里学到的态度:
工具是用来完成工作的,不是用来争论的。
选你熟悉的工具,快速完成项目,比花时间争论"哪个语言更好"有意义得多。
我用两个语言都写了一遍编译器,花的时间可能够写三个不同的项目了。这种对比本身就是浪费时间,除非你就是为了学习。
实用建议
如果你要开始一个编译器项目:
已经会 C++?直接用 C++,别纠结
已经会 Rust?直接用 Rust,别纠结
两个都不会?
- 想快速出成果 → 学 Python(是的,很多编译器用 Python 写)
- 想学系统编程 → 随便选一个,重要的是开始写
最关键的:
- ✅ 先把编译器写出来
- ❌ 别陷入"完美工具"的陷阱
语言只是工具
- GCC 用 C++ 写的,很强大
- Rust 编译器本身用 Rust 写的,也很强大
- CPython 用 C 写的,还是很强大
- PyPy 用 Python 写的,依然很强大
关键不是语言,是你对编译器原理的理解。
总结
写完两个版本后,我的真实感受:
- Rust 的开发体验确实更好(对我来说)
- C++ 也完全能胜任,只是需要更小心
- 但这些都不重要,重要的是把项目完成
如果你也想写编译器:
- 别花时间纠结语言
- 选一个你熟悉的(或想学的)
- 然后开始写
工具服务于目标,不要本末倒置。
P.S. 写完这篇文章后我意识到,花时间写这篇"对比文章"本身可能就是在浪费时间。如果你也在纠结语言选择,不如直接开始写代码,边做边学。真正的经验来自实践,不是来自文章。
参考资料
编译器学习:
- Crafting Interpreters - 强烈推荐,用 Java 和 C 实现
- Writing An Interpreter In Go - 用 Go 写解释器
语言特性:
- Rust Book - Rust 官方教程
- Modern C++ Features - C++11/14/17/20 特性
- C++ Core Guidelines - C++ 最佳实践
关于工具选择的思考:
- Linus Torvalds 谈工具 - 务实的态度
- The Bike Shed Effect - 为什么我们会在工具选择上浪费时间
修改于 2025-11-06