|
|
@@ -0,0 +1,197 @@ |
|
|
.. title:: clang-tidy - misc-use-after-move |
|
|
|
|
|
misc-use-after-move |
|
|
=================== |
|
|
|
|
|
Warns if an object is used after it has been moved, for example: |
|
|
|
|
|
.. code-block:: c++ |
|
|
|
|
|
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: |
|
|
|
|
|
.. code-block:: c++ |
|
|
|
|
|
messages.emplace_back(std::move(str)); |
|
|
str = "Greetings, stranger!\n"; |
|
|
std::cout << str; |
|
|
|
|
|
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: |
|
|
|
|
|
.. code-block:: c++ |
|
|
|
|
|
if (condition) { |
|
|
messages.emplace_back(std::move(str)); |
|
|
} else { |
|
|
std::cout << str; |
|
|
} |
|
|
|
|
|
On the other hand, the following code does produce a warning: |
|
|
|
|
|
.. code-block:: c++ |
|
|
|
|
|
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): |
|
|
|
|
|
.. code-block:: c++ |
|
|
|
|
|
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. |
|
|
|
|
|
An erroneous warning can be silenced by reinitializing the object after the |
|
|
move: |
|
|
|
|
|
.. code-block:: c++ |
|
|
|
|
|
if (i == 1) { |
|
|
messages.emplace_back(std::move(str)); |
|
|
str = ""; |
|
|
} |
|
|
if (i == 2) { |
|
|
std::cout << str; |
|
|
} |
|
|
|
|
|
No warnings are emitted for objects of type ``std::unique_ptr`` and |
|
|
``std::shared_ptr``, as they have defined move behavior. (Objects of these |
|
|
classes are guaranteed to be empty after they have been moved from.) |
|
|
|
|
|
Subsections below explain more precisely what exactly the check considers to be |
|
|
a move, use, and reinitialization. |
|
|
|
|
|
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: |
|
|
|
|
|
.. code-block:: c++ |
|
|
|
|
|
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: |
|
|
|
|
|
.. code-block:: c++ |
|
|
|
|
|
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. |
|
|
|
|
|
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. |
|
|
|
|
|
Use |
|
|
--- |
|
|
|
|
|
Any occurrence of the moved variable that is not a reinitialization (see below) |
|
|
is considered to be a use. |
|
|
|
|
|
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()`` or ``assign()`` is called on the variable and the variable is of |
|
|
one of the standard container types ``basic_string``, ``vector``, ``deque``, |
|
|
``forward_list``, ``list``, ``set``, ``map``, ``multiset``, ``multimap``, |
|
|
``unordered_set``, ``unordered_map``, ``unordered_multiset``, |
|
|
``unordered_multimap``. |
|
|
|
|
|
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: |
|
|
|
|
|
.. code-block:: c++ |
|
|
|
|
|
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. |