CppCon 2018 学习:Undefined Behavior is Not an Error

发布于:2025-07-07 ⋅ 阅读:(20) ⋅ 点赞:(0)

这段内容进一步深入探讨了 未定义行为(Undefined Behavior, UB) 对 C++ 编译器的影响,特别是编译器如何优化包含 UB 的代码,以及一些具体的 UB 示例。我们可以将这些要点理解为以下几个方面:

1. 未来的编译器与未定义行为

  • 恶意编译器(Evil Compiler): 如果编译器被设计得过于激进,它可能会利用未定义行为进行优化,甚至改变程序的执行方式。例如,编译器可能假设某些未定义行为不可能发生,从而重排代码,产生意想不到的结果。
  • 高效编译器(Efficient Compiler): 一个高效的编译器会基于 “未定义行为不可能发生” 的假设,优化代码。它可能会删除所有可能导致 UB 的代码,甚至对代码顺序进行重排,从而获得更好的性能。
  • 未定义行为的意外好处: 有时,程序可能因为 UB 而得到一些奇怪的优化效果,可能会导致看似“有用”的意外行为。

2. 编译器选项和 UB

  • 禁用优化时的行为: 如果编译器没有启用优化,它通常会尽量忠实地翻译代码,使得代码执行尽可能接近源代码的字面含义。在这种情况下,未定义行为可能看起来不会造成问题,程序可能会按预期工作。
  • 启用优化时的行为: 启用优化时,编译器可能会移除一些不可达的代码,并且不会诊断出 UB。编译器可能会通过内联或其他优化措施改变代码的执行顺序,这可能会导致程序在存在未定义行为时产生不可预测的结果。

3. 未定义行为的具体示例

示例 2:缺少返回值
  • 在一个“值返回函数”中,缺少返回语句会导致 UB。编译器可能会给出警告,但程序执行时可能会崩溃、始终返回一个固定值,或者跳转到下一个函数。
bool isGreen() {
    m_data == "green";
}
示例 3:数组越界访问
  • 访问一个容器(如字符串)中超出边界的元素会导致 UB。即使你通过 inputStr[index+1]inputStr[index+2] 访问元素时没有做越界检查,也会导致 UB。
std::string inputStr = "class std::vector<int>";
std::string className;
for (int index = 0; index < inputStr.size(); ++index) { 
    if (inputStr[index+1] == ':' && inputStr[index+2] == ':') { 
        // found start of class name
        index += 2;
        className = inputStr.substr(index); // want vector<int>
    }  
}
示例 4:调试时的 UB
  • 使用 printf() 调试崩溃时,程序的行为可能会因为调用的顺序而发生变化。这是由于某些函数调用(例如使用旧版本的 QString)引入了 UB,导致静态初始化顺序问题。解决方案是通过静态方法内部局部静态变量来避免这一问题。
示例 5:指针的关系比较
  • 比较两个指针只有在它们指向同一对象或同一数组的元素时才有定义。如果尝试比较两个不同对象或数组的指针,会导致 UB,但标准中规定这种比较行为为“未指定行为”(Unspecified Behavior),这意味着编译器有自由决定其行为。
int a = 0;
int b = 0;
return &a < &b;  // 未指定行为
示例 6:整数溢出
  • max 值非常大时,求和的结果可能会导致有符号整数溢出,触发 UB。高效的编译器会意识到 UB 不能发生,因此会优化代码。但程序员仍然有责任确保该函数不会被传递一个过大的值。
int sum_numbers(int max) {
    int retval = 0;
    for (int cnt = 1; cnt <= max; ++cnt) {
        retval += cnt;
    }
    return retval;
}
示例 7:修改 const 对象
  • 如果你通过 const_cast 去除 const 修饰符并修改原本是 const 的对象,行为会变得不可预测,属于 UB。
void doThing7(const std::string& str) {
    std::string& tmp = const_cast<std::string&>(str);
    tmp = "new information";   
}
示例 8:标准类型特征的特化
  • 特化标准类型特征(例如 std::is_pointer)是 UB。如果允许这样的代码,它会破坏标准类型特征的行为,导致程序的不可预期结果。
namespace std {
    template<>
    struct is_pointer<int> : public std::true_type { };
}
bool test = std::is_pointer<int>::value;  // UB

4. 顺序点(Sequence Point)

  • 顺序点定义: 顺序点是源代码中的一个位置,通常在一个表达式的末尾,所有前面表达式的副作用(如变量的修改)都已经完成,之后的表达式尚未计算。顺序点保证了编译器在优化时不会打乱副作用的顺序。
  • 顺序点的作用: 编译器在优化时必须遵循顺序点的约束,不能改变表达式计算的顺序。顺序点的概念自 C++ 语言起就存在,并且是编译器优化和程序正确性的重要基础。

总结

这段内容强调了 未定义行为 对编译器优化的深远影响以及程序员如何管理和避免 UB。编译器可能会在优化时对 UB 进行“合理”的处理,但这也可能导致一些意外的程序行为。理解 UB 是每个 C++ 程序员必须掌握的技能,避免 UB 有助于编写健壮、安全和高效的代码。
这段内容深入讨论了 副作用(Side Effect)表达式顺序(Sequencing) 在 C++ 中与 未定义行为(UB) 的关系,特别是在 C++11 和 C++17 标准中的变化。让我们将这部分分解并理解:

1. 副作用(Side Effect)

  • 副作用 是指在表达式计算过程中,除了返回值之外,对外部状态进行的修改。常见的副作用包括:
    • 读取 volatile 对象
    • 写访问任何对象
    • 调用执行 I/O 操作的库函数
    • 调用修改状态的函数(例如修改全局变量、输出到控制台等)
      示例分析:
  • varA = 5;:这是一个副作用,因为它修改了 varA 的值。
  • varB = 2 + --varA;:这里也有副作用,varA 被修改了,并且在计算结果时使用了 varA 的修改值。

2. 表达式的顺序(Sequencing)

  • 顺序:C++11 引入了更精确的表达式顺序管理方式,逐步取代了传统的“顺序点(sequence points)”概念。
    • 表达式 A 先于 B 被顺序化:即 A 在 B 之前执行。
    • 不确定顺序(Indeterminate Sequencing):表示两个表达式的顺序不可知。
    • 无序(Unsequenced):表示两个表达式的顺序没有明确的规定,它们的计算可能是重叠的,且无法预测。

3. 未定义行为的示例

示例 9:副作用顺序问题
  • C++03 中,表达式 ++varA + 2 存在两个副作用——++varA 和赋值给 varA。由于它们的顺序未定义,导致 未定义行为
    • varA = 5; 正常,不产生 UB。
    • varA = ++varA + 2; 在 C++03 中会导致 UB,因为 ++varA 和赋值操作的顺序不确定。
    • varB = varB++ + 2; 也是 UB,在 C++03 中,因为递增操作与加法没有顺序保证。
      C++11 和 C++17 的变化:
  • C++11 中,varA = ++varA + 2 不再是未定义行为,varA == 8 是已定义行为。
  • varB = varB++ + 2 在 C++11 中仍是 UB,而在 C++17 中是已定义行为。
示例 10:函数参数的顺序
  • C++17 将函数参数的评估顺序从“不确定”改变为“无序”。
  • 函数参数的评估现在在函数调用之前按顺序进行,但没有明确规定参数之间的评估顺序。换句话说,如果你写如下代码:
    int var1;
    planDinner(var1 = doThing1(), doThing2(var1));
    
    由于 C++17 中对函数参数顺序的处理,doThing1()doThing2(var1) 的调用顺序不再明确,但 doThing1() 仍然会先于 doThing2(var1) 被执行。
示例 11:数组下标中的副作用
  • 表达式 myArray[varB++] = varB++ + 3; 是一个典型的未定义行为示例。此处涉及多个副作用:
    • varB++varB++ 具有副作用,它们的顺序未定义。
    • 在 C++11 中,这会导致未定义行为,因为赋值操作和自增操作的顺序不确定。
    • 但在 C++17 中,这个行为变为已定义,因为 C++17 引入了新的规则:对于数组下标表达式 E1[E2]E1E2 的值计算以及副作用的顺序会被严格规定。即,E1 的所有副作用必须在 E2 之前完成。

4. 总结

  • 副作用 是 C++ 编程中不可忽视的重要概念,特别是当多个副作用发生时,可能导致未定义行为。理解副作用的顺序对于编写正确的 C++ 程序至关重要。
  • 表达式顺序 在 C++11 和 C++17 中发生了变化,从以前的“顺序点”到更精确的“顺序”和“无序”表达式控制。
  • C++11C++17 中,标准对表达式评估顺序进行了更严格的规范,减少了未定义行为的可能性,尤其是在函数参数和数组下标的处理中。
    如果你在编写 C++ 代码时能理解这些副作用的处理方式并正确管理表达式顺序,能显著减少潜在的未定义行为,提高代码的健壮性和可维护性。
    这段内容继续深入探讨 未定义行为(Undefined Behavior, UB) 在 C++ 中的不同方面,包括如何在编译过程中检测 UB,特别是在 C++17 中的变化以及如何避免 UB。以下是对主要概念和示例的解析:

1. C++17 编译器警告

  • C++17 标准对一些表达式的行为进行了更严格的定义,从而消除了许多以前的未定义行为。然而,现有的编译器(例如 GCC 和 Clang)仍会发出警告,提醒开发者可能存在 UB。这些警告是为了帮助开发者避免在早期 C++ 版本中存在的问题。
    警告示例:
  • GCC 7.3 和 8.2:operation on 'varB' may be undefined [-Wsequence-point]
  • Clang 6.0 和 7.0:unsequenced modification and access to 'varB' [-Wunsequenced]
    这些警告是因为在表达式 myArray[varB++] = varB++ + 3; 中,varB++ 两次递增操作的顺序未明确规定,虽然在 C++17 中这些表达式不再是未定义行为,但编译器仍然给出警告,提醒开发者在过去的 C++ 标准中,这种写法会导致 UB。
    总结:
  • C++17 确定了某些操作的顺序,确保了原本的 UB 被修正,但编译器出于兼容性的考虑,仍然显示警告,帮助开发者避免在旧版本中出错的代码。

2. 未定义行为:QFlatMap

  • QFlatMap 是一种新的数据结构,用于存储按键排序的键值对,类似于 std::map,但它是通过一个 std::vector 来实现的。其优点包括:
    • 存储在连续内存中,节省内存。
    • 适用于较小的数据集。
      然而,C++ 标准并没有定义类似 QFlatMap 的容器,因此它属于 未定义行为。开发者可以通过 QFlatMap 实现类似 std::map 的功能,但这种实现并不是标准化的,因此可能会导致未定义行为。
      关键点:
  • QFlatMap 不是 C++ 标准库的一部分,它是特定于某个实现(如 Qt)的一种数据结构,虽然它的 API 设计类似于 std::map,但并未在 C++ 标准中明确规定,因此它的行为可能是未定义的。

3. 未文档化的未定义行为

  • 根据 C++ 标准(C++17),未明确指定的行为被视为未定义行为。这适用于标准中没有明确定义为“已定义行为”或“错误”的情况。例如,QFlatMap 可能在特定实现中引入新的功能和行为,但如果这些行为没有标准化,它们可能会导致未定义行为。
    重要概念:
  • 未定义行为 是指标准未明确定义其行为的任何代码。对于标准未明确规定的行为,开发者必须小心,确保代码的行为符合预期。

4. 避免未定义行为的建议

为了避免程序中的未定义行为,开发者可以采取以下措施:

  • 注意编译器警告:编译器会提醒开发者代码中的潜在问题,及时处理这些警告有助于避免 UB。
  • 阅读 C++ 标准或参考文档:如 cppreference.com,帮助开发者理解哪些行为是合法的,哪些可能导致 UB。
  • 使用多个编译器进行测试:不同的编译器可能会对某些边界情况产生不同的处理,使用多个编译器可以发现潜在的问题。
  • 进行代码审查:团队成员之间的代码审查可以帮助发现潜在的 UB。
  • 测试极端情况:尽量测试所有可能的边界情况和特殊情况,以确保代码的健壮性。
  • 将未定义行为视为严重 Bug:任何未定义行为都可能导致程序崩溃或无法预料的行为,因此应该重视它。
  • 使用静态分析工具
    • Coverity
    • Clang Static Analyzer
    • Purify
  • 这些工具可以帮助开发者静态地分析代码中的潜在问题,提前发现 UB。

5. 内存和行为分析工具

  • Clang 提供了多种工具来检测和修复潜在的 UB,包括:
    • AddressSanitizer (ASan):检查内存错误。
    • MemorySanitizer (MSan):检测未初始化的内存。
    • UndefinedBehaviorSanitizer (UBSan):专门检测 UB。
    • ThreadSanitizer (TSan):用于多线程程序的并发错误检测。
  • GCC 也提供了类似的工具:
    • ASan:用于检查内存错误。
    • UBSan:用于检测未定义行为。
  • Valgrind:一个第三方工具,结合了 ASan 和 UBSan,用于检测内存管理错误和未定义行为。

6. 总结

  • C++17 对一些未定义行为进行了更严格的定义,减少了不确定性,但编译器仍然会发出警告,提醒开发者在旧版本的 C++ 中可能出现的问题。
  • 未定义行为 的处理和检测至关重要,开发者应当遵循标准、重视警告、使用静态分析工具,并进行全面测试,以避免 UB。
  • 对于像 QFlatMap 这样的非标准数据结构,开发者需要确保它们在实现中是符合预期的,并且不会引发未定义行为。