CppCon 2018 学习:Applied Best Practices
主要是实现强类型包装(strong typing),使得类型不易混淆(例如防止不同含义的整数相互赋值),且实现合理的访问控制和性能保证。
template<typename Type, typename CTRP>
struct Strongly_Typed {// 1. 静态断言:Type 必须是平凡类型(trivial),保证简单高效static_assert(std::is_trivial_v<Type>);// 2. 成员变量,存储实际值
protected:Type m_value;// 3. 访问器函数,返回存储的值// - constexpr:支持编译期常量求值,方便优化和静态断言// - const:不修改对象状态// - noexcept:保证不抛异常,提高稳定性和性能// - [[nodiscard]]:提醒调用者不要忽略返回值,防止遗漏[[nodiscard]] constexpr auto value() const noexcept {return m_value;}
};
设计和实现分析
- 模板参数
Type
:底层存储类型,比如std::uint32_t
。CTRP
:CRTP 技术参数(Curiously Recurring Template Pattern),通常是子类类型,用来实现静态多态和类型区分。
- 静态断言
static_assert(std::is_trivial_v<Type>)
确保Type
是平凡类型(POD-like),这样包装不会带来额外复杂性,也允许编译器做优化。
- 成员变量
m_value
- 受保护,防止外部直接修改,保证封装。
- 不是
const
,允许构造后可修改(如果需要可额外设计只读接口)。
- 访问器
value()
- 返回底层值的副本(通常是小型类型,拷贝成本低)。
- 用
constexpr
和noexcept
表明函数简单、安全、可在编译期使用。 [[nodiscard]]
是现代C++的好习惯,提醒调用者注意返回值,防止无意忽略。
可能的扩展和考虑
- 构造函数:这里没写,但实际使用时需要构造函数把值传进去。
- 运算符重载:比较、加减等重载,保证强类型的使用便利。
- 赋值和复制:确保类型安全和语义明确。
- CRTP用途:利用
CTRP
让不同强类型间不兼容,防止混用。
总结
这个模板是设计强类型封装的经典小框架,关注:
- 类型安全:用不同包装区分相同底层类型的不同语义。
- 性能:用平凡类型和内联访问器保证开销小。
- 代码可维护性:访问器、封装和现代C++属性增强接口安全和可读性。
C++ 函数的返回类型写法,尤其是“尾置返回类型(trailing return types)”和普通返回类型的写法对比,理解这些写法有助于写出更清晰和灵活的代码。
1. 三种写法比较
以一个访问器函数 value()
为例:
// A
[[nodiscard]] constexpr auto value() const noexcept { return m_value; }
// B
[[nodiscard]] constexpr auto value() const noexcept -> Type { return m_value; }
// C
[[nodiscard]] constexpr Type value() const noexcept { return m_value; }
说明:
- A写法:用
auto
让编译器自动推断返回类型,代码简洁。 - B写法:用“尾置返回类型”写法,声明
-> Type
在函数签名尾部,常见于泛型编程或返回类型复杂时。 - C写法:直接写明确的返回类型。
2. 什么时候用哪种写法?
- 如果返回类型简单(如内置类型,或者类型已明确),推荐用 C 或 A 写法。
- 如果返回类型复杂(如模板推导依赖、或者返回类型过长难写),尾置返回类型(B)写法有优势。
3. 复杂返回类型示例
// A: 尾置返回类型,声明放后面
[[nodiscard]] constexpr auto op() const noexcept -> std::pair<bool, uint32_t>;
[[nodiscard]] constexpr auto type() const noexcept -> Op_Type;
// B: 普通写法,直接写完整类型
[[nodiscard]] constexpr std::pair<bool, uint32_t> op() const noexcept;
[[nodiscard]] constexpr Op_Type type() const noexcept;
尾置返回类型的主要用途:
- 函数返回类型依赖于模板参数或者函数参数时,必须用尾置返回类型。
- 写代码时更易读,尤其返回类型复杂或模板时。
4. 总结理解
写法 | 适用场景 | 优点 | 缺点 |
---|---|---|---|
auto 返回类型(无尾置) | 简单、直接,返回类型已知 | 简洁,自动推断 | 无法用于依赖模板参数的返回类型 |
尾置返回类型 | 返回类型复杂、依赖模板参数或需写完整类型 | 明确返回类型,易读,必要时必须用 | 语法稍复杂,写起来冗长 |
明确返回类型 | 返回类型简单,明确 | 清晰,直观 | 代码重复,返回类型变化时修改麻烦 |
你可以根据函数的返回类型复杂度选择合适的写法: |
- 简单返回值用
auto
(A写法)或者直接写类型(C写法)。 - 复杂返回类型用尾置返回类型(B写法),保证清晰且符合标准。
有人更喜欢用**尾置返回类型(trailing return types)**的写法,原因是:
- 当函数声明中有很多修饰符(比如
[[nodiscard]]
、constexpr
、noexcept
等属性)时, - 如果返回类型写在函数名前面(传统写法),函数名会被这些修饰符“淹没”,不够醒目,
- 用尾置返回类型写法(
auto func() -> ReturnType
),函数名写在返回类型之前,显得更清晰、更突出。
简单说就是:
尾置返回类型让函数名更醒目,不容易被各种修饰符淹没,所以很多人喜欢用这种写法。
这段代码定义了一个 System
结构体,结合之前“强类型”和“尾置返回类型”的讨论,具体功能和设计如下:
代码分析
struct System {// 模板构造函数,接收一个固定大小的 uint8_t 数组作为内存初始化数据template<std::size_t Size> constexpr System(const std::array<std::uint8_t, Size> &memory) noexcept{// 静态断言确保传入的内存大小不能超过系统允许的最大内存大小static_assert(Size <= RAM_Size);// 将传入的每个字节写入系统内存中对应的位置for (std::size_t loc = 0; loc < Size; ++loc) {// 这里将 size_t 转换为 uint32_t,静态断言保证转换安全write_byte(static_cast<std::uint32_t>(loc), memory[loc]);}// 填充指令缓存,假设 i_cache 是系统的指令缓存结构i_cache.fill_cache(*this);}// 返回是否还有剩余操作,调用者不能忽略返回值[[nodiscard]] constexpr bool ops_remaining() const noexcept {// 具体实现省略}// 根据程序计数器 PC,获取一个操作指令,返回强类型 Op[[nodiscard]] constexpr auto get(const uint32_t PC) noexcept -> Op {// 具体实现省略}// 设置系统运行的起始位置constexpr void setup_run(const std::uint32_t loc) noexcept{// 将寄存器 14(通常是栈指针)设置为内存顶端减4,留出空间registers[14] = RAM_Size - 4;// 设置程序计数器为 loc + 4,通常是跳过启动代码的前4字节PC() = loc + 4;}// 其他成员和函数省略...
};
理解和关键点
- 模板构造函数
System
通过接受一个固定大小的字节数组memory
来初始化。- 这里的
Size
是数组大小的模板参数,静态断言确保输入大小不超过RAM_Size
(假设系统最大内存大小)。 - 循环调用
write_byte
,把传入的字节写入内存地址,地址是loc
转换为uint32_t
,安全可靠。
- 缓存初始化
- 构造函数最后调用
i_cache.fill_cache(*this)
,这里假设是对系统状态的某种缓存预处理或加速。
- 构造函数最后调用
- 成员函数
ops_remaining()
- 返回一个
bool
,表明是否还有剩余的操作需要执行。 [[nodiscard]]
表明调用者不能忽略返回值。constexpr
和noexcept
说明这个函数很轻量且不会抛异常。
- 返回一个
- 成员函数
get()
- 这是个访问器,返回类型是之前定义过的强类型
Op
。 - 采用了尾置返回类型写法,体现了之前“尾置返回类型”的风格。
- 作用是基于程序计数器
PC
获取指令或操作。
- 这是个访问器,返回类型是之前定义过的强类型
- 成员函数
setup_run()
- 初始化或设置运行状态。
registers[14]
可能是栈指针或类似寄存器,被设置为内存顶端减4(预留空间)。PC()
可能是程序计数器访问函数,设为loc + 4
,开始执行位置。
总结
- 这段代码体现了“强类型”“尾置返回类型”和“constexpr noexcept”风格的现代 C++设计。
- 模板参数让初始化灵活且安全(静态检测大小)。
- 通过明确函数修饰,保证函数调用安全且高效。
- 代码结构清晰,注重类型安全和性能。
constexpr
在 C++ 中的使用,特别是用在 CPU 模拟器(emulator)中的体会和优势。总结如下:
1. 什么不能是 constexpr
?
- 答案是:几乎没有什么不能是
constexpr
!
现代 C++(C++20 及以后)极大扩展了constexpr
的能力,支持更多复杂逻辑,几乎任何代码都可以写成constexpr
。
2. constexpr
的缺点
- 必须放在头文件中
因为模板和constexpr
函数需要在编译时展开,代码通常放在头文件,导致编译单元之间的依赖增加。 - 编译时间可能变长
不过,这主要是因为大符号表和长名字,和constexpr
本身没有必然联系。合理设计代码不会大幅增加编译时间。
3. constexpr
的优点
- 能够在编译时执行复杂的 CPU 模拟
这意味着代码在编译期间已经完成了指令的模拟和执行,运行时无需解释,性能更好且更安全。 - 保证代码正确
代码如果能通过编译,模拟器的测试就已经成功了(编译期测试)。
4. 代码示例说明
TEST_CASE("Test arbitrary movs")
{// 机器码指令// 0: e3a000e9 mov r0, #233// 4: e3a0100c mov r1, #12// 在编译时运行模拟器constexpr auto system = run(0xe9,0x00,0xa0,0xe3,0x0c,0x10,0xa0,0xe3);// 编译期断言寄存器值是否正确REQUIRE(static_test<system.registers[0] == 233>());REQUIRE(static_test<system.registers[1] == 12>());
}
- 这段测试代码利用
constexpr
,在编译时执行 CPU 模拟器的run
函数。 REQUIRE(static_test<...>())
也是编译期断言,确保寄存器的值如预期。- 如果代码能编译通过,就说明模拟器逻辑正确,功能测试通过。
总结
- 用
constexpr
写 CPU 模拟器,能让整个模拟过程在编译期执行,提升安全性和性能。 - 虽然可能增加代码复杂度和编译依赖,但不一定大幅影响编译速度。
- 现代 C++ 对
constexpr
支持非常好,几乎没有限制。
下面是这段示例代码加上详细注释的版本,帮助你理解 constexpr
如何帮助捕捉未定义行为:
// 普通函数,执行左移操作
auto shift(int val, int distance)
{// 左移val,位移distance位return val << distance;
}
int main()
{// 这里调用shift,将1左移32位// 对于32位int来说,左移32位是未定义行为auto result = shift(1, 32);// 返回值是不确定的,程序可能返回任意值,甚至出现错误return result;
}
// 将函数声明为constexpr,表示它在编译时也能执行
constexpr auto shift(int val, int distance)
{// 执行左移操作return val << distance;
}
int main()
{// constexpr变量,编译器要求此处结果在编译时即可求出constexpr auto result = shift(1, 32);// 因为左移32位越界,这里会在编译期检测出未定义行为// 导致编译失败,编译器报错,防止潜在的运行时bugreturn result;
}
代码注释总结
- 普通函数
shift
中的未定义行为会在运行时产生不可预期的结果。 constexpr
强制在编译时求值,因此编译器必须确保操作是合法的。- 如果操作是未定义的(如左移位数超过类型宽度),编译器会报错,拒绝生成代码。
- 使用
constexpr
可以让开发者更早、更明确地发现隐藏的未定义行为,提升代码安全性。
额外说明
- 这种编译期检测可以捕捉编译器警告可能漏掉的问题。
- 它让代码在更早阶段进行“自我验证”。
- 对于CPU模拟器、嵌入式、关键系统代码非常有用。
constexpr
和未定义行为(Undefined Behavior, UB)之间的关系,核心观点如下:
主要理解点
- 不同编译器对
constexpr
中未定义行为的处理不完全一致- 有些编译器会严格禁止
constexpr
函数中出现未定义行为,导致编译失败。 - 另一些可能容忍某些未定义行为,或者没有完全检查,表现可能不一样。
- 所以,为了保证代码的可移植性和稳定性,必须在多种编译器下测试。
- 有些编译器会严格禁止
- 启用全
constexpr
支持和在编译期进行测试有助于- 让程序员更早地发现潜在的未定义行为。
- 通过编译期的约束,增加代码的安全性和可靠性。
- 这对复杂项目(如 CPU 模拟器)尤为重要。
你可以这样理解:
- 传统的运行时测试可能无法捕获所有未定义行为(尤其是某些隐藏的逻辑错误)。
- 使用
constexpr
并结合编译期测试,可以提前“验证”代码行为是否符合预期。 - 但因为不同编译器的实现差异,仍需在多编译器、多平台上进行充分测试,确保代码符合标准且无UB。
C++ 里默认的函数修饰符(constexpr
、noexcept
、[[nodiscard]]
等)以及如果“反转”这些默认值,会带来怎样的影响和思考。
主要内容和理解
- 看到一个函数声明:
这是一个非常“严谨”的函数,带有很多默认修饰:[[nodiscard]] constexpr auto value() const noexcept -> Type { return m_value; }
[[nodiscard]]
:调用者不应该忽略返回值。constexpr
:可以在编译期求值。const
:不修改成员变量。noexcept
:函数不会抛异常。
- 作者问:“我们对这些默认修饰怎么看?是不是太严格了?”
- 假设“所有默认修饰都反转”:
[[nodiscard]]
变成[[discardable]]
(允许丢弃返回值,C++ 里其实没有这个属性,作者假设它存在)。constexpr
被取消,变成普通函数。const
取消,允许函数修改状态(加了mutable
)。noexcept(false)
:函数默认可能抛异常。
- 代码示例变成:
auto value() -> Type { return m_value; } [[discardable]] auto change_things() mutable noexcept(false) -> Type;
- 这是在反思 C++ 现在的“严格”默认值是否合适。
- 虽然现实中不能直接反转这些默认值,但类似的想法在 C++ 中以某种方式存在,比如 lambda 表达式:
auto value = [this]() [[nodiscard]] noexcept -> Type { /*return something*/ };
- 这里,lambda 默认是
constexpr
(C++20 之后),且返回类型只写一次,语法更简洁,和上面讨论的设计思路有点类似。
总结
- C++ 的默认函数修饰(
const
、noexcept
、constexpr
、[[nodiscard]]
)都是为了安全和效率设计的。 - 作者让我们思考:如果默认行为相反(比如函数允许抛异常、可以修改对象状态、返回值可以被丢弃),代码会变成什么样?
- 虽然不能直接修改语言的默认,但通过 lambda 等特性,我们可以看到部分设计思路的变通。
- 这对理解函数设计和写 API 有启发,提醒我们要注意默认行为对代码可维护性和安全性的影响。
关于在 C++ 项目(尤其是支持 constexpr
的复杂项目)中积累的经验总结,主要围绕代码质量、工具使用、编译器行为和团队协作,核心理解如下:
Lessons Learned(经验总结)
1. 记住重要修饰符需要自律
noexcept
、[[nodiscard]]
等修饰符虽然很重要,但开发时容易忘记加,需要有意识去养成习惯。- 如果你假设函数默认是
constexpr
,就容易保持代码在“编译期可执行”的状态,降低引入运行时错误的风险。 - 必须写
constexpr
测试,确保代码真的能在编译期正确运行。
2. constexpr
有助于捕获未定义行为
- 编译期求值限制了很多未定义行为出现的机会,提前暴露潜在问题。
3. 格式化工具的重要性
- 使用
clang-format
是近乎必须的。 - 虽然太严格的代码风格会影响表达力,但不规范的代码更难维护和审查。
- 对新贡献者,要求运行
clang-format
可以极大提高代码库一致性和代码质量。
4. 对未知领域(新库、新工具)的谨慎
- 当你不了解某个库或工具时,很容易写出杂乱、无序的代码,需要刻意保持自律。
5. 编译期代码放置建议
- 虽然很多代码需要放在头文件(如
constexpr
代码),但依然建议把非constexpr
代码放在.cpp
文件:- 依赖外部库的代码放
.cpp
,隔离不稳定或实验性的代码。 - 需要动态内存的代码也放
.cpp
,保持头文件简洁。
- 依赖外部库的代码放
6. constexpr
不是重点,而是手段
- 这个演讲的核心不是单纯推广
constexpr
,而是指出在constexpr
限制下写的代码,实际上就是一种“理想的 C++ 子集”:- 几乎没有未定义行为
- 没有异常
- 无动态分配(或者极少)
- 类型简单、易于复制
- 移动语义不重要(因为类型可平凡复制)
7. 构建系统的重要性
- 构建系统配置不好,会漏报警告或者导致奇怪的问题。
- 演讲者自己在原型阶段没有警告,正式配置好构建系统后发现大量警告,说明严格的构建系统对代码质量和安全非常关键。
总结
这是一份非常实用的经验分享,强调:
- 工具(格式化、构建系统)和习惯(修饰符、测试)是代码质量的基石
constexpr
是实现高质量、安全、无UB代码的有效途径,但不是最终目的- 保持代码整洁、模块划分清晰,对维护大型项目至关重要
启用足够严格的编译器警告选项的重要性和实际效果,尤其是在项目初期的原型阶段。
理解要点:
1. 启用警告是必需的
- 演讲者开始用的命令行:
看似已经挺全面了,但其实还不够。g++ -std=c++17 -Wall -Wextra -Wshadow -Wpedantic test.cpp
2. 详细的警告选项清单
- 实际上,除了上述选项外,还有很多有用的警告可以打开:
-Wnon-virtual-dtor
:类有虚函数但析构函数非虚时警告-Wold-style-cast
:警告 C 风格强制类型转换-Wcast-align
:警告可能的性能影响的类型转换-Wunused
:警告未使用变量、函数等-Woverloaded-virtual
:警告重载但非覆盖虚函数-Wconversion
、-Wsign-conversion
:警告可能导致数据丢失或符号转换-Wnull-dereference
:警告空指针解引用-Wdouble-promotion
:警告 float 到 double 的隐式提升-Wformat=2
:格式化字符串相关的安全警告-Wduplicated-cond
、-Wduplicated-branches
:警告重复的条件或代码块-Wlogical-op
:警告逻辑操作符误用为位操作符-Wuseless-cast
:警告无效的类型转换- 以及最新的如
-Wlifetime
等
3. 启用所有这些警告的效果
- 即使是一个很小的原型项目,也可能因为代码中存在隐患和不规范而产生大量警告。
- 如果没有从一开始就启用这些警告,代码中的潜在问题就容易被忽视,随着项目规模扩大,问题会积累变难修复。
总结
- 一开始就用严格的编译器警告选项,能在早期就暴露潜在的问题。
- 即使是简单的原型代码,也不能放松警告标准。
- 养成良好编译器警告策略,可以节省后期调试和维护大量时间。
- 这个经验适合所有C++开发者,尤其是对大型项目或库开发尤为重要。
C++中隐式转换的问题,以及启用 -Wconversion
警告的重要性和现实中的挑战。
理解要点:
1. 隐式转换是C++的“痛点”
- 很多人认为,如果能去掉C++的某个特性,“隐式转换” 会是首选目标。
- 隐式转换往往导致难以发现的bug,比如数据截断、符号位转换错误等。
2. -Wconversion
警告的重要性
- 这个警告能帮你发现隐式转换带来的潜在风险,比如:
- 有符号整型转无符号整型
- 整型提升或截断
- 浮点数转整型等
- 但往往开发者不喜欢启用它,因为它会带来大量警告,很多看似无害但又得去修正的代码。
3. 代码示例说明
- PC() += offset + 4;
+ PC() += static_cast<std::uint32_t>(offset + 4); // rely on 2's complement
- 这里,
offset + 4
的类型可能是有符号整型,隐式转换为uint32_t
时会触发警告。 - 显式用
static_cast<std::uint32_t>
转换,不仅消除警告,也明确表达了意图,依赖的是二进制补码行为(2’s complement)。
总结
- 隐式转换带来的问题被广泛关注,开启
-Wconversion
警告能有效捕捉相关隐患。 - 尽管有时候修正这些警告可能烦琐,但这是保持代码健壮和可维护的好习惯。
- 显式转换是解决隐式转换警告的好办法,同时让代码意图更清晰。
这段代码及其背景的详细解释和注释,帮你理解关键点以及如何改进代码来避免警告和潜在的错误:
// 定义枚举类 Types,只有两个有效枚举值
enum class Types { Option1, Option2 };
// 结构体 T1 和 T2,分别用来包装 Types 枚举值
struct T1 { Types v; };
struct T2 { Types v; };
// 声明两个处理函数,接受不同的结构体
int handle(T1);
int handle(T2);
// 函数 process,根据传入的 Types 参数调用不同的处理函数
auto process(Types t) {switch (t) {case Types::Option1: return handle(T1{t});case Types::Option2: return handle(T2{t});}// 注意这里没有默认分支,也没有返回语句// 如果传入的 t 不是 Option1 或 Option2,函数没有返回值,会导致编译器警告
}
int main() {// 这里通过 static_cast 强制转换整数 3 为 Types 类型// 这并不是合法的 Types 枚举值,可能导致未定义行为process(static_cast<Types>(3));
}
代码存在的问题和警告
- 警告:“Not all paths return a value”
switch
没有覆盖所有可能的枚举值。- 如果
t
传入了一个非法值(例如static_cast<Types>(3)
),函数没有返回值,导致未定义行为。
enum class
可以被强制转换为不在枚举定义中的值static_cast<Types>(3)
是合法语法,但不是有效的枚举值。- C++ 标准允许这种情况,但使用非法枚举值会导致逻辑错误或未定义行为。
如何改进和避免警告
1. 添加 default
分支
为 switch
添加一个 default
分支来处理未覆盖的情况,防止函数没有返回值:
auto process(Types t) {switch (t) {case Types::Option1: return handle(T1{t});case Types::Option2: return handle(T2{t});default: // 处理非法枚举值,比如抛异常,返回默认值或断言throw std::runtime_error("Invalid Types value");}
}
或者返回一个默认值:
auto process(Types t) {switch (t) {case Types::Option1: return handle(T1{t});case Types::Option2: return handle(T2{t});}// 防止没有返回值return -1; // 或者其他合适的返回值
}
2. 使用 [[nodiscard]]
和其他属性
可以通过 [[nodiscard]]
强制调用者注意返回值,增加代码安全。
3. 枚举范围检查
如果不希望非法值进入函数,可以在调用前进行范围检查:
bool isValidType(Types t) {return t == Types::Option1 || t == Types::Option2;
}
int main() {Types t = static_cast<Types>(3);if (!isValidType(t)) {// 处理错误} else {process(t);}
}
总结
- 枚举类型
enum class
在 C++ 中并不会自动防止非法值,需要额外保护。 switch
语句覆盖不全时编译器会给警告,要么添加default
分支,要么确保覆盖所有可能值。- 调用
process(static_cast<Types>(3))
是未定义行为,应避免。 - 编写安全代码时,显式处理非法情况非常重要。
C++ 教学讲座的一部分,主要讨论的是 枚举 (enum class
) 与 switch
语句中未覆盖所有枚举值所产生的警告、潜在的未定义行为 (UB)、以及处理方式,并引出 constexpr
场景下的特殊要求。
场景回顾
enum class Types { Option1, Option2 };
struct T1 { Types v; };
struct T2 { Types v; };
int handle(T1); // 处理 Option1 的情况
int handle(T2); // 处理 Option2 的情况
auto process(Types t) {switch (t) {case Types::Option1: return handle(T1{t});case Types::Option2: return handle(T2{t});}// 如何优雅处理未覆盖值?
}
枚举类型的底层表示
enum class Types { Option1, Option2 };
- 默认情况下,
enum class
的底层类型是int
,除非显式指定。 - 所以
Types
的有效值范围其实是所有int
值 —— 并不仅仅是Option1
、Option2
。 - 这意味着下面代码是合法语法,但可能造成未定义行为:
process(static_cast<Types>(42)); // 不是 Option1 或 Option2
switch 警告的根源
编译器提示:“not all control paths return a value”,是因为 switch
并未覆盖所有 Types
的可能取值。
错误行为
// 没有 default,也没有 return,会有警告
合法的解决方案(逐个来看)
方法 1:使用 assert
#include <cassert>
auto process(Types t) {switch (t) {case Types::Option1: return handle(T1{t});case Types::Option2: return handle(T2{t});}assert(!"Cannot reach here!"); // 仅在 Debug 模式有效
}
- 优点:调试期间可以发现非法输入。
- 缺点:Release 模式下
assert
被移除,问题不会暴露。
方法 2:使用 std::abort()
#include <cstdlib>
auto process(Types t) {switch (t) {case Types::Option1: return handle(T1{t});case Types::Option2: return handle(T2{t});}abort(); // 无条件终止程序,适合硬错误场景
}
- 优点:Release 模式下也能阻止继续执行。
- 缺点:无法“优雅”地恢复程序。
方法 3:抛出异常
#include <stdexcept>
auto process(Types t) {switch (t) {case Types::Option1: return handle(T1{t});case Types::Option2: return handle(T2{t});}throw std::runtime_error("Unhandled Opcode");
}
- 优点:最灵活,可以在上层捕获异常做错误处理。
- 缺点:不适用于
constexpr
场景(constexpr
中不能使用异常)。
方法 4:constexpr
场景下的策略
auto process(Types t) {switch (t) {case Types::Option1: return handle(T1{t});case Types::Option2: return handle(T2{t});}unhandled_instruction(t); // 标记出错状态的回调(编译期检查器用)return {}; // 返回默认值(必须是可构造的)
}
unhandled_instruction(t)
可能是一个constexpr
函数,做静态分析、标志位设置等。return {}
要求handle
的返回类型是支持默认构造的,例如int
、std::optional<int>
。- 优点:兼容
constexpr
,安全。 - 缺点:需要设计配合的错误处理策略。
小结:处理未覆盖 enum class
的 switch
的几种方式
方法 | 安全性 | 性能影响 | 支持 constexpr | 备注 |
---|---|---|---|---|
assert | 中 | ⬜ 无影响 | 否 | 调试期间有效 |
abort() | 高 | ⬜ 无影响 | 否 | 适合硬崩溃、嵌入式等 |
throw | 高 | 有影响 | 否 | 正常应用中推荐 |
constexpr fallback | 高 | 最佳 | 是 | 最适合 constexpr 场景 |
关于 构建系统(Build System)、包管理(Package Management) 和 C++ 文化心态 的一些经验教训。以下是逐页解读与注释:
小抱怨结束,回到 Build System(构建系统)
Jason 说:
“我有大约 10 年过时的 CMake 使用经验,跟上时代是很值得的。”
要点:
- CMake 是 C++ 社区最主流的构建系统。
- 即使你经验丰富,也可能使用了“老旧方式”。
- 推荐工具:
cmake-format
—— 保持 CMakeLists.txt 清晰统一。
建议: - 使用现代 CMake(目标导向、模块化、
target_link_libraries()
等)。 - 使用工具自动格式化和验证(比如:
cmake-lint
,cmake-format
)。
包管理(Package Management)
核心观点:
“现代 C++ 应该利用包管理器。”
Jason 的项目依赖以下库:
库名 | 用途说明 |
---|---|
SFML | 图形、多媒体库 |
rang | 控制终端颜色的轻量级库 |
Catch2 | 单元测试框架 |
spdlog | 快速日志库(候选) |
{fmt} | 字符串格式化库,C++20 std::format 的前身 |
Jason 曾评估的包管理器(截至 2018)
工具名 | 简介 |
---|---|
Buckaroo | 后来停止维护 |
build2 + cppget | 整合式构建和包管理系统 |
Conan (Center) | 跨平台流行包管理器(活跃) |
CPPAN | 一种 C++ 包发布方式 |
Hunter | CMake 驱动的包下载工具 |
qpm | 轻量级包管理工具 |
vcpkg | 微软出品,广泛使用,集成 VSCode 支持 |
当时 Jason 认为最实用的是:Conan 和 vcpkg。 |
“我又造了一个轮子” - 再造轮子反思
Jason 的例子:
“我写了个 ELF parser,我堂弟(Rust 开发)5 分钟用现成 crate 就搞定了。”
核心反思:
“我甚至没想到要去找已有的 C++ ELF 解析库。”
文化问题:
- C++ 开发者习惯从零写代码。
- 而 Rust/Python/JS 开发者先找库再决定造轮不造轮。
Jason 的建议
“C++ 社区需要转变思维方式,先找有没有可用的库。”
这是一个对整个 C++ 社区文化的批评和建议:
不良习惯 | 建议变更 |
---|---|
惯性手写一切 | 先搜索是否有已有、可复用库 |
忽视第三方依赖许可 | 加入自动 license 检查器是个好主意 |
低估安全漏洞影响 | 包管理器应支持已知漏洞的提示/阻止 |
小结与建议
使用包管理器推荐:
工具 | 适用范围 |
---|---|
vcpkg | Windows 用户、VS 用户首选 |
Conan | 多平台项目、CI/CD 环境 |
文化改进建议: |
- 优先寻找库而不是直接编码。
- 管理依赖安全性(Licenses & Known CVEs)。
- 写构建系统时保持现代化、整洁性(使用格式化工具)。