当前位置: 首页 > news >正文

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;}
};

设计和实现分析

  1. 模板参数
    • Type:底层存储类型,比如 std::uint32_t
    • CTRP:CRTP 技术参数(Curiously Recurring Template Pattern),通常是子类类型,用来实现静态多态和类型区分。
  2. 静态断言
    • static_assert(std::is_trivial_v<Type>) 确保 Type 是平凡类型(POD-like),这样包装不会带来额外复杂性,也允许编译器做优化。
  3. 成员变量 m_value
    • 受保护,防止外部直接修改,保证封装。
    • 不是 const,允许构造后可修改(如果需要可额外设计只读接口)。
  4. 访问器 value()
    • 返回底层值的副本(通常是小型类型,拷贝成本低)。
    • constexprnoexcept 表明函数简单、安全、可在编译期使用。
    • [[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. 什么时候用哪种写法?

  • 如果返回类型简单(如内置类型,或者类型已明确),推荐用 CA 写法。
  • 如果返回类型复杂(如模板推导依赖、或者返回类型过长难写),尾置返回类型(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]]constexprnoexcept 等属性)时,
  • 如果返回类型写在函数名前面(传统写法),函数名会被这些修饰符“淹没”,不够醒目,
  • 用尾置返回类型写法(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;}// 其他成员和函数省略...
};

理解和关键点

  1. 模板构造函数
    • System 通过接受一个固定大小的字节数组 memory 来初始化。
    • 这里的 Size 是数组大小的模板参数,静态断言确保输入大小不超过 RAM_Size(假设系统最大内存大小)。
    • 循环调用 write_byte,把传入的字节写入内存地址,地址是 loc 转换为 uint32_t,安全可靠。
  2. 缓存初始化
    • 构造函数最后调用 i_cache.fill_cache(*this),这里假设是对系统状态的某种缓存预处理或加速。
  3. 成员函数 ops_remaining()
    • 返回一个 bool,表明是否还有剩余的操作需要执行。
    • [[nodiscard]] 表明调用者不能忽略返回值。
    • constexprnoexcept 说明这个函数很轻量且不会抛异常。
  4. 成员函数 get()
    • 这是个访问器,返回类型是之前定义过的强类型 Op
    • 采用了尾置返回类型写法,体现了之前“尾置返回类型”的风格。
    • 作用是基于程序计数器 PC 获取指令或操作。
  5. 成员函数 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)之间的关系,核心观点如下:

主要理解点

  1. 不同编译器对 constexpr 中未定义行为的处理不完全一致
    • 有些编译器会严格禁止 constexpr 函数中出现未定义行为,导致编译失败。
    • 另一些可能容忍某些未定义行为,或者没有完全检查,表现可能不一样。
    • 所以,为了保证代码的可移植性和稳定性,必须在多种编译器下测试。
  2. 启用全 constexpr 支持和在编译期进行测试有助于
    • 让程序员更早地发现潜在的未定义行为。
    • 通过编译期的约束,增加代码的安全性和可靠性。
    • 这对复杂项目(如 CPU 模拟器)尤为重要。

你可以这样理解:

  • 传统的运行时测试可能无法捕获所有未定义行为(尤其是某些隐藏的逻辑错误)。
  • 使用 constexpr 并结合编译期测试,可以提前“验证”代码行为是否符合预期。
  • 但因为不同编译器的实现差异,仍需在多编译器、多平台上进行充分测试,确保代码符合标准且无UB。

C++ 里默认的函数修饰符(constexprnoexcept[[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++ 的默认函数修饰(constnoexceptconstexpr[[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));
}

代码存在的问题和警告

  1. 警告:“Not all paths return a value”
    • switch 没有覆盖所有可能的枚举值。
    • 如果 t 传入了一个非法值(例如 static_cast<Types>(3)),函数没有返回值,导致未定义行为。
  2. 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 值 —— 并不仅仅是 Option1Option2
  • 这意味着下面代码是合法语法,但可能造成未定义行为
    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 的返回类型是支持默认构造的,例如 intstd::optional<int>
  • 优点:兼容 constexpr,安全。
  • 缺点:需要设计配合的错误处理策略。

小结:处理未覆盖 enum classswitch 的几种方式

方法安全性性能影响支持 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++ 包发布方式
HunterCMake 驱动的包下载工具
qpm轻量级包管理工具
vcpkg微软出品,广泛使用,集成 VSCode 支持
当时 Jason 认为最实用的是:Conanvcpkg

“我又造了一个轮子” - 再造轮子反思

Jason 的例子:

“我写了个 ELF parser,我堂弟(Rust 开发)5 分钟用现成 crate 就搞定了。”

核心反思

“我甚至没想到要去找已有的 C++ ELF 解析库。”

文化问题

  • C++ 开发者习惯从零写代码
  • 而 Rust/Python/JS 开发者先找库再决定造轮不造轮。

Jason 的建议

“C++ 社区需要转变思维方式,先找有没有可用的库。”

这是一个对整个 C++ 社区文化的批评和建议:

不良习惯建议变更
惯性手写一切先搜索是否有已有、可复用库
忽视第三方依赖许可加入自动 license 检查器是个好主意
低估安全漏洞影响包管理器应支持已知漏洞的提示/阻止

小结与建议

使用包管理器推荐

工具适用范围
vcpkgWindows 用户、VS 用户首选
Conan多平台项目、CI/CD 环境
文化改进建议
  • 优先寻找库而不是直接编码。
  • 管理依赖安全性(Licenses & Known CVEs)。
  • 写构建系统时保持现代化、整洁性(使用格式化工具)。
http://www.dtcms.com/a/263451.html

相关文章:

  • APP 内存测试--Android Memory Profiler实操(入门版)
  • ACE之ACE_NonBlocking_Connect_Handler问题分析
  • 【FineDataLink快速入门】01界面介绍-运维中心
  • AI教育全景图:谁在领跑2025?
  • 【Debian】1- 安装Debian到物理主机
  • STM32——DAP下载程序和程序调试
  • 【C++】经典string类问题
  • 【数字人开发】结合nextHuman平台进行数字人网页端开发
  • VMware 在局域网环境将虚拟机内部ip 端口开放
  • 【读代码】TradingAgents:基于多智能体LLM的金融交易框架深度解析
  • STM32 rs485实现中断DMA模式收发不定长数据
  • STM32-第一节-新建工程,GPIO输出(LED,蜂鸣器)
  • SQuirreL SQL:一个免费的通用数据库开发工具
  • 华为云Flexus+DeepSeek征文 | 基于华为云Dify-LLM搭建知识库问答助手
  • 怎么在手机上预约心理咨询师
  • MySQL索引失效场景
  • 【软考高项论文】信息系统项目的资源管理
  • 大模型在急性左心衰竭预测与临床方案制定中的应用研究
  • 【Redis面试篇】Redis高频八股汇总
  • 长短期记忆网络(LSTM):让神经网络拥有 “持久记忆力” 的神奇魔法
  • 周赛98补题
  • Go语言安装使用教程
  • Golang的多环境配置
  • 「Java流程控制」while循环
  • Redis 实现消息队列
  • 【软考高项论文】论信息系统项目的质量管理
  • js代码01
  • 【数据分析】环境数据降维与聚类分析教程:从PCA到可视化
  • uniapp+vue2 input不显示明文密码,点击小眼睛显示或隐藏密码
  • “对象创建”模式之原型模式