bugprone-use-after-move¶
Warns if an object is used after it has been moved, for example:
std::string str = "Hello, world!\n";
std::vector<std::string> messages;
messages.emplace_back(std::move(str));
std::cout << str;
The last line will trigger a warning that str
is used after it has been
moved.
The check does not trigger a warning if the object is reinitialized after the move and before the use. For example, no warning will be output for this code:
messages.emplace_back(std::move(str));
str = "Greetings, stranger!\n";
std::cout << str;
Subsections below explain more precisely what exactly the check considers to be a move, use, and reinitialization.
The check takes control flow into account. A warning is only emitted if the use can be reached from the move. This means that the following code does not produce a warning:
if (condition) {
messages.emplace_back(std::move(str));
} else {
std::cout << str;
}
On the other hand, the following code does produce a warning:
for (int i = 0; i < 10; ++i) {
std::cout << str;
messages.emplace_back(std::move(str));
}
(The use-after-move happens on the second iteration of the loop.)
In some cases, the check may not be able to detect that two branches are
mutually exclusive. For example (assuming that i
is an int):
if (i == 1) {
messages.emplace_back(std::move(str));
}
if (i == 2) {
std::cout << str;
}
In this case, the check will erroneously produce a warning, even though it is not possible for both the move and the use to be executed. More formally, the analysis is flow-sensitive but not path-sensitive.
Silencing erroneous warnings¶
An erroneous warning can be silenced by reinitializing the object after the move:
if (i == 1) {
messages.emplace_back(std::move(str));
str = "";
}
if (i == 2) {
std::cout << str;
}
If you want to avoid the overhead of actually reinitializing the object, you can create a dummy function that causes the check to assume the object was reinitialized:
template <class T>
void IS_INITIALIZED(T&) {}
You can use this as follows:
if (i == 1) {
messages.emplace_back(std::move(str));
}
if (i == 2) {
IS_INITIALIZED(str);
std::cout << str;
}
The check will not output a warning in this case because passing the object to a function as a non-const pointer or reference counts as a reinitialization (see section Reinitialization below).
Unsequenced moves, uses, and reinitializations¶
In many cases, C++ does not make any guarantees about the order in which sub-expressions of a statement are evaluated. This means that in code like the following, it is not guaranteed whether the use will happen before or after the move:
void f(int i, std::vector<int> v);
std::vector<int> v = { 1, 2, 3 };
f(v[1], std::move(v));
In this kind of situation, the check will note that the use and move are unsequenced.
The check will also take sequencing rules into account when reinitializations occur in the same statement as moves or uses. A reinitialization is only considered to reinitialize a variable if it is guaranteed to be evaluated after the move and before the use.
Move¶
The check currently only considers calls of std::move
on local variables or
function parameters. It does not check moves of member variables or global
variables.
Any call of std::move
on a variable is considered to cause a move of that
variable, even if the result of std::move
is not passed to an rvalue
reference parameter.
This means that the check will flag a use-after-move even on a type that does
not define a move constructor or move assignment operator. This is intentional.
Developers may use std::move
on such a type in the expectation that the type
will add move semantics in the future. If such a std::move
has the potential
to cause a use-after-move, we want to warn about it even if the type does not
implement move semantics yet.
Furthermore, if the result of std::move
is passed to an rvalue reference
parameter, this will always be considered to cause a move, even if the function
that consumes this parameter does not move from it, or if it does so only
conditionally. For example, in the following situation, the check will assume
that a move always takes place:
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));
The check will assume that the last line causes a move, even though, in this particular case, it does not. Again, this is intentional.
There is one special case: A call to std::move
inside a try_emplace
call
is conservatively assumed not to move. This is to avoid spurious warnings, as
the check has no way to reason about the bool
returned by try_emplace
.
When analyzing the order in which moves, uses and reinitializations happen (see
section Unsequenced moves, uses, and reinitializations), the move is assumed
to occur in whichever function the result of the std::move
is passed to.
The check also handles perfect-forwarding with std::forward
so the
following code will also trigger a use-after-move warning.
void consume(int);
void f(int&& i) {
consume(std::forward<int>(i));
consume(std::forward<int>(i)); // use-after-move
}
Use¶
Any occurrence of the moved variable that is not a reinitialization (see below) is considered to be a use.
An exception to this are objects of type std::unique_ptr
,
std::shared_ptr
, std::weak_ptr
, std::optional
, and std::any
.
An exception to this are objects of type std::unique_ptr
,
std::shared_ptr
, std::weak_ptr
, std::optional
, and std::any
, which
can be reinitialized via reset
. For smart pointers specifically, the
moved-from objects have a well-defined state of being nullptr``s, and only
``operator*
, operator->
and operator[]
are considered bad accesses as
they would be dereferencing a nullptr
.
If multiple uses occur after a move, only the first of these is flagged.
Reinitialization¶
The check considers a variable to be reinitialized in the following cases:
The variable occurs on the left-hand side of an assignment.
The variable is passed to a function as a non-const pointer or non-const lvalue reference. (It is assumed that the variable may be an out-parameter for the function.)
clear()
orassign()
is called on the variable and the variable is of one of the standard container typesbasic_string
,vector
,deque
,forward_list
,list
,set
,map
,multiset
,multimap
,unordered_set
,unordered_map
,unordered_multiset
,unordered_multimap
.
reset()
is called on the variable and the variable is of typestd::unique_ptr
,std::shared_ptr
,std::weak_ptr
,std::optional
, orstd::any
.A member function marked with the
[[clang::reinitializes]]
attribute is called on the variable.
If the variable in question is a struct and an individual member variable of that struct is written to, the check does not consider this to be a reinitialization – even if, eventually, all member variables of the struct are written to. For example:
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;
The check will not consider s
to be reinitialized after the last line;
instead, the line that assigns to s.str
will be flagged as a use-after-move.
This is intentional as this pattern of reinitializing a struct is error-prone.
For example, if an additional member variable is added to S
, it is easy to
forget to add the reinitialization for this additional member. Instead, it is
safer to assign to the entire struct in one go, and this will also avoid the
use-after-move warning.