C++ 基础概念: 未定义行为(Undefined Behavior)

概览

在编程中我们会听到或者看到一个概念:“未定义行为(Undefined Behavior, 简称 UB)”. 什么是所谓的未定义行为, 会产生什么后果, 如何能避免? 本文将系统地探讨未定义行为的含义, 后果及规避方法

如何正确认识 UB

对"未定义行为"的字面理解就是: 这个行为没有被具体说明. 举个例子, 如果读到了 std::vector<T> 的末尾会发生什么?

可能得结果有:

因为根据 C++ 标准:

reading past the end of std::vector is undefined behavior

读取超过 std::vector 末尾的内容是未定义的行为

因为没有做具体说明, 所以编译器可以做很多选择. 然而这些选择不一定是编写代码的程序员所期望的.

有多少未定义行为?

举个例子, 用 26 个英语字母作组合可以产生非常多单词. 排除掉字典里面定义的单词, 其他的单词可以被认为是"未定义的", 可想而知这些未定义的情况非常多的. 同样的, C++语言中的 UB 的具体 case 也会非常多, 不可枚举.

对 UB 的误解

一些常见的误解如下:

后续的例子中我们将会消除这些迷思.

C++ 标准定义的几种行为

C++标准中定义了如下几种行为:

1. 定义的行为 (defined behavior)

具有明确或精确含义的代码, 比如:

2. 实现定义的行为 (implementation defined behavior)

代码可以有多重含义, 但编译器必须选定一种并始终保持该选择. 请看下面的代码:

if ( sizeof(int) < sizeof(long) ) { }

C++ 标准中规定了int最小要有 16bit, long最小要有 32bit. 具体 bit 位数会因为编译器/操作系统而有所不同, 常见的编译器 GCC, Clang, MSVC 指定了 sizeof(int) == 4, sizeof(long)

3. 未指定的行为 (unspecified behavior)

代码可能有多种含义, 编译器可以随机选择一个. 比如比较字符串字面量:

#include <iostream>

void fun(const char* key) {
  if (key == "name") {
    std::cout << "get name\n";
  } else {
    std::cout << "something else\n";
  }
}
int main() {
  std::string name = "name";
  fun("name");        // output: get name
  fun(name.c_str());  // output: something else
  return 0;
}

比较字面量在实际中被实现为比较指针. 而程序员预期的应该是字符串比较.

4. 未定义行为 (undefined behavior)

毫无意义的代码, 比如:

阅读下面的代码思考一下这两个问题:

  1. 下面的代码能通过编译吗?
  2. 是否有 UB, 有的话指出具体行数.
#include <iostream>

int main() {
  int* p = nullptr;  // line 1
  *p = 42;           // line 2
  int b;             // line 3
  p = &b;            // line 4
  std::cout << *p;   // line 5
  std::cout << b;    // line 6
}

揭晓答案

  1. 能通过编译
  2. 有下面这些 UB
    • ( line 2 ) 解析空指针是 UB
    • ( line 5 和 line 6) 访问一个未初始化的变量 UB

C++ 中如何定义 UB

  1. 所谓 UB 就是尝试去执行那种没有被 C++标准明确说明其行为的代码.
  2. 只有当源代码没有 UB 时, 程序会按源代码所写的执行
  3. 如果你的代码有 UB, 那么 C++ 标准将对其执行结果不做任何保证
  4. 编写没有 UB 的代码是程序员的责任

UB 不是错误

常见 C++ UB 的部分列表

软件设计理念

编译器选项对 UB 的影响

当关闭编译器优化时

通常会启用优化

如何消除 UB

  1. 借助工具

    • Address Sanitizer
    • Memory Sanitizer
    • Undefined Behavior Sanitizer
    • Thread Sanitizer
  2. 代码审查, 制定专门检查 UB 的政策

  3. 注意编译器警告

  4. 使用多个编译器构建代码

  5. 测试极端情况

  6. 将 UB 视为严重错误

UB 举例

带符号的整型溢出

#include <iostream>

template <typename T>
T cubic(T len) {
  return len * len * len;
}

int main() {
  std::cout << "cubic signed: " << cubic(3000) << std::endl;     // UB
  std::cout << "cubic unsigned: " << cubic(3000u) << std::endl;  // OK
  return 0;
}

缺失 return 语句

一些编译器会发出警告, 一些清理程序会在运行时检测到. 程序执行过程中的常见结果

#include <iostream>

bool baz() { return true; }
bool foo(int a, int b) { a == b; }
bool bar() { return false; }

int main() {
  int a = 1;
  int b = 2;
  std::cout << "a == b: " << foo(a, b) << std::endl;
  std::cout << bar() << baz() << std::endl;
  return 0;
}

迭代器在使用时被破坏

容器上的某些操作会使迭代器无效, std::vector::insert() 使所有迭代器无效.

#include <iostream>
#include <vector>

int main() {
  std::vector<int> vec = {1, 2, 3, 5, 6};
  for (auto &item : vec) {
    if (item == 3) {
      vec.insert(vec.begin(), 4);
    }
    std::cout << item << std::endl;
  }
  return 0;
}

修改const reference类型值

关键字 const_cast 删除对象的"常量性", 如果传递的参数最初被声明为 const, 则修改输入是未定义的行为

#include <iostream>
#include <string>

const std::string global = "Hello";

void fun(const std::string &input) {
  std::string &v = const_cast<std::string &>(input);
  v = "fun";
}

int main() {
  const std::string local = "World";
  fun(local);
  std::cout << local << std::endl;

  fun(global);
  std::cout << global << std::endl;
  return 0;
}

std命名空间中增加代码

偏特化 std 命名空间中存在的类型特征是 UB. 编写自己的类型特征是完全可以接受的, 它们可以 位于除 std:: 之外的任何命名空间中.

#include <iostream>
#include <type_traits>

namespace std {
template <>
struct is_pointer<int> : public std::true_type  // defines a type trait as true
{};
}  // namespace std

int main() {
  bool var2 = std::is_pointer<int>::value;
  std::cout << std::boolalpha << std::is_pointer<int>::value << std::endl;
  return 0;
}

求值顺序

#include <iostream>

int main() {
  int a = 5;
  a = ++a + 2;  // C++03, undefined behavior
  a == 8;       // C++11 and newer, defined
  std::cout << "a: " << a << std::endl;

  int b = 3;
  b = b++ + 2;  // C++03 and C++11, undefined behavior
  b == 5;       // C++17 and newer, defined
  std::cout << "b: " << b << std::endl;
}

语法歧义

这个函数有未定义的行为吗?

#include <iostream>

template <typename T1, typename T2>
void fun(T1 &x, T2 &y) {
  x << y;
}

int main() {
  int a = 1;
  int b = 1000;
  fun(a, b);              // UB: 左移操作的移动位数超过了类型的宽度
  fun(std::cout, "cat");  // OK
}

我们看到这个例子中是否有 UB 取决于入参, 以及入参的具体值.

C++ 为了更明确模板的行为, 推出了新特性"Concept"概念, 用来约束模板参数. 方便开发者避免此类问题.

总结

视频链接

源码链接

源码链接