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_ptr
、std::shared_ptr
和 std::weak_ptr
的对象,它们定义了移动行为(保证这些类的对象在从中移动后为空)。因此,只有在对这些类的对象进行解引用时才会将它们视为被使用,即只有在对它们调用 operator*
、operator->
或 operator[]
(在 std::unique_ptr<T []>
的情况下)时。
如果在移动后发生多次使用,则只标记第一个使用。
重新初始化¶
检查在以下情况下将变量视为重新初始化
该变量出现在赋值的左侧。
该变量作为非 const 指针或非 const 左值引用传递给函数。(假设该变量可能是函数的输出参数。)
对变量调用
clear()
或assign()
,并且该变量属于以下标准容器类型之一:basic_string
、vector
、deque
、forward_list
、list
、set
、map
、multiset
、multimap
、unordered_set
、unordered_map
、unordered_multiset
、unordered_multimap
。对变量调用
reset()
,并且该变量属于类型std::unique_ptr
、std::shared_ptr
或std::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
添加了额外的成员变量,很容易忘记添加此额外成员变量的重新初始化。相反,将整个结构体一次性分配给它是更安全的,这也会避免使用后移动警告。