CodeQL / Analyze (actions) (push) Canceled after 0s
CodeQL / Analyze (python) (push) Canceled after 0s
pre-commit / pre-commit (push) Canceled after 0s
Multi-Arch CI / setup (push) Canceled after 0s
Publish build_manylinux_x86_64 images / publish_build_manylinux_x86_64 (push) Canceled after 0s
Setup (Multi-Arch) / setup (push) Canceled after 0s
Multi-Arch CI / Linux::${{ fromJSON(needs.setup.outputs.linux_build_config || '{}').build_variant_label || 'skip' }} (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish no_rocm_image_ubi10 image / publish_no_rocm_image_ubi10 (push) Canceled after 0s
Multi-Arch CI / Windows::${{ fromJSON(needs.setup.outputs.windows_build_config || '{}').build_variant_label || 'skip' }} (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Multi-Arch CI / Manifest Diff (push) Canceled after 0s
TheRock Manifest Diff Report / Generate Manifest Diff Report (push) Canceled after 0s
Multi-Arch CI / CI Summary (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04 (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04_ocl_rt images / publish_no_rocm_image_ubuntu24_04_ocl_rt (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_rocgdb (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04_rocgdb images / publish_no_rocm_image_ubuntu24_04_rocgdb (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_ocl_rt (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Unit Tests / Unit Tests :: ubuntu-24.04 (push) Canceled after 0s
Unit Tests / Unit Tests :: windows-2022 (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_media (push) Canceled after 0s
Unit Tests / Unit Tests Summary (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_openmpi (push) Canceled after 0s
282 lines
9.6 KiB
ReStructuredText
282 lines
9.6 KiB
ReStructuredText
.. 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.
|
|
|
|
The check also handles perfect-forwarding with ``std::forward`` so the
|
|
following code will also trigger a use-after-move warning.
|
|
|
|
.. code-block:: c++
|
|
|
|
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)
|
|
or an explicit call to the variable destructor 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``,
|
|
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``.
|
|
|
|
User-defined types can be annotated as having the same semantics as standard
|
|
smart pointers with ``[[clang::annotate("clang-tidy",
|
|
"bugprone-use-after-move", "null_after_move")]]``. This expresses that a
|
|
moved-from object of this type is a null pointer.
|
|
|
|
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``, ``std::weak_ptr``,
|
|
``std::optional``, or ``std::any``.
|
|
|
|
- A member function marked with the ``[[clang::reinitializes]]`` attribute is
|
|
called on the variable.
|
|
|
|
- The variable is passed as an argument to ``std::tie`` on the left-hand
|
|
side of an assignment (e.g. ``std::tie(a, b) = f(...)``). The tuple
|
|
assignment operator writes back through the stored references, which
|
|
reinitializes each named 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.
|
|
|
|
Options
|
|
-------
|
|
|
|
.. option:: InvalidationFunctions
|
|
|
|
A semicolon-separated list of regular expressions matching names of functions
|
|
that cause their first arguments to be invalidated (e.g., closing a handle).
|
|
For member functions, the first argument is considered to be the implicit
|
|
object argument (``this``). Default value is an empty string.
|
|
|
|
.. option:: ReinitializationFunctions
|
|
|
|
A semicolon-separated list of regular expressions matching names of functions
|
|
that reinitialize the object. For member functions, the implicit object
|
|
argument (``*this``) is considered to be reinitialized. For non-member or
|
|
static member functions, the first argument is considered to be
|
|
reinitialized. Default value is an empty string.
|