bugprone-use-after-move

如果对象在移动后被使用,则发出警告,例如

std::string str = "Hello, world!\n";
std::vector<std::string> messages;
messages.emplace_back(std::move(str));
std::cout << str;

最后一行将触发警告,表明 str 在移动后被使用。

如果对象在移动后并在使用前重新初始化,则检查不会触发警告。例如,此代码不会输出任何警告

messages.emplace_back(std::move(str));
str = "Greetings, stranger!\n";
std::cout << str;

以下小节更详细地解释了检查究竟将什么视为移动、使用和重新初始化。

检查会考虑控制流。仅当从移动操作到达使用操作时才会发出警告。这意味着以下代码不会产生警告

if (condition) {
  messages.emplace_back(std::move(str));
} else {
  std::cout << str;
}

另一方面,以下代码会产生警告

for (int i = 0; i < 10; ++i) {
  std::cout << str;
  messages.emplace_back(std::move(str));
}

(使用后移动发生在循环的第二次迭代中。)

在某些情况下,检查可能无法检测到两个分支是互斥的。例如(假设 i 是一个 int)

if (i == 1) {
  messages.emplace_back(std::move(str));
}
if (i == 2) {
  std::cout << str;
}

在这种情况下,即使移动和使用不可能同时执行,检查也会错误地发出警告。更正式地说,分析是 流敏感但不是路径敏感 的。

消除错误警告

可以通过在移动后重新初始化对象来消除错误警告

if (i == 1) {
  messages.emplace_back(std::move(str));
  str = "";
}
if (i == 2) {
  std::cout << str;
}

如果要避免实际重新初始化对象的开销,可以创建一个虚拟函数,该函数会使检查假设对象已被重新初始化

template <class T>
void IS_INITIALIZED(T&) {}

您可以按如下方式使用它

if (i == 1) {
  messages.emplace_back(std::move(str));
}
if (i == 2) {
  IS_INITIALIZED(str);
  std::cout << str;
}

在这种情况下,检查不会输出警告,因为将对象作为非 const 指针或引用传递给函数被视为重新初始化(请参阅下面的 重新初始化 部分)。

未排序的移动、使用和重新初始化

在许多情况下,C++ 不会对语句的子表达式求值的顺序做出任何保证。这意味着在以下代码中,不能保证使用是否会在移动之前或之后发生

void f(int i, std::vector<int> v);
std::vector<int> v = { 1, 2, 3 };
f(v[1], std::move(v));

在这种情况下,检查会注意到使用和移动是未排序的。

当在与移动或使用相同的语句中发生重新初始化时,检查也会考虑排序规则。只有在保证重新初始化在移动之后并在使用之前求值时,它才会被视为重新初始化变量。

移动

检查目前只考虑对局部变量或函数参数调用 std::move。它不检查成员变量或全局变量的移动。

对变量的任何 std::move 调用都将被视为导致该变量的移动,即使 std::move 的结果没有传递给右值引用参数。

这意味着即使在没有定义移动构造函数或移动赋值运算符的类型上,检查也会标记使用后移动。这是故意的。开发人员可能会在该类型上使用 std::move,期望该类型将来会添加移动语义。如果这种 std::move 有可能导致使用后移动,即使该类型尚未实现移动语义,我们也希望发出警告。

此外,如果 std::move 的结果确实传递给了右值引用参数,这将始终被视为导致移动,即使使用该参数的函数没有从该参数中移动,或者它仅在条件下移动。例如,在以下情况下,检查会假设总是发生移动

std::vector<std::string> messages;
void f(std::string &&str) {
  // Only remember the message if it isn't empty.
  if (!str.empty()) {
    messages.emplace_back(std::move(str));
  }
}
std::string str = "";
f(std::move(str));

检查将假设最后一行会导致移动,即使在这种情况中没有发生。同样,这是故意的。

有一个特殊情况:在 try_emplace 调用中调用 std::move 被保守地假设为不移动。这是为了避免虚假警告,因为检查无法推断 try_emplace 返回的 bool 的值。

当分析移动、使用和重新初始化发生的顺序时(请参阅 未排序的移动、使用和重新初始化 部分),移动将被假定为发生在传递 std::move 结果的函数中。

检查还处理使用 std::forward 的完美转发,因此以下代码也会触发使用后移动警告。

void consume(int);

void f(int&& i) {
  consume(std::forward<int>(i));
  consume(std::forward<int>(i)); // use-after-move
}

使用

移动变量的任何不属于重新初始化(见下文)的出现都被视为使用。

一个例外是类型为 std::unique_ptrstd::shared_ptrstd::weak_ptr 的对象,它们定义了移动行为(保证这些类的对象在从中移动后为空)。因此,只有在对这些类的对象进行解引用时才会将它们视为被使用,即只有在对它们调用 operator*operator->operator[](在 std::unique_ptr<T []> 的情况下)时。

如果在移动后发生多次使用,则只标记第一个使用。

重新初始化

检查在以下情况下将变量视为重新初始化

  • 该变量出现在赋值的左侧。

  • 该变量作为非 const 指针或非 const 左值引用传递给函数。(假设该变量可能是函数的输出参数。)

  • 对变量调用 clear()assign(),并且该变量属于以下标准容器类型之一:basic_stringvectordequeforward_listlistsetmapmultisetmultimapunordered_setunordered_mapunordered_multisetunordered_multimap

  • 对变量调用 reset(),并且该变量属于类型 std::unique_ptrstd::shared_ptrstd::weak_ptr

  • 对变量调用带有 [[clang::reinitializes]] 属性的成员函数。

如果相关变量是一个结构体,并且该结构体的单个成员变量被写入,则检查不会将其视为重新初始化,即使最终所有结构体成员变量都被写入。例如

struct S {
  std::string str;
  int i;
};
S s = { "Hello, world!\n", 42 };
S s_other = std::move(s);
s.str = "Lorem ipsum";
s.i = 99;

检查不会认为 s 在最后一行之后被重新初始化;相反,将分配给 s.str 的行将被标记为使用后移动。这是故意的,因为这种重新初始化结构体的模式容易出错。例如,如果向 S 添加了额外的成员变量,很容易忘记添加此额外成员变量的重新初始化。相反,将整个结构体一次性分配给它是更安全的,这也会避免使用后移动警告。