blob: 8509292eff9947723ba6227bcc0d430278805dd6 [file] [log] [blame]
.. title:: clang-tidy - bugprone-use-after-move
bugprone-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;
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:
.. 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. More formally, the
analysis is `flow-sensitive but not path-sensitive
<https://en.wikipedia.org/wiki/Data-flow_analysis#Sensitivities>`_.
Silencing erroneous warnings
----------------------------
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;
}
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:
.. code-block:: c++
template <class T>
void IS_INITIALIZED(T&) {}
You can use this as follows:
.. code-block:: c++
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:
.. 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.
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.
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`` and ``std::weak_ptr``, which have defined move behavior
(objects of these classes are guaranteed to be empty after they have been moved
from). Therefore, an object of these classes will only be considered to be used
if it is dereferenced, i.e. if ``operator*``, ``operator->`` or ``operator[]``
(in the case of ``std::unique_ptr<T []>``) is called on it.
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``.
- ``reset()`` is called on the variable and the variable is of type
``std::unique_ptr``, ``std::shared_ptr`` or ``std::weak_ptr``.
- 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:
.. 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.