Rvalue reference arguments allow the efficient transfer of the ownership of objects. Therefore, it is expected that rvalue arguments or their
subobjects are, conditionally or not, moved into their destination variables.
The ownership is unclear when an rvalue argument, including its subobject or elements, is never moved. This might lead to bugs and performance
issues.
This rule does not apply when the argument is a forwarding reference.
Exceptions
For the C++23 or later standard, this rule does not raise issues if the function returns the rvalue reference parameter. In such cases, the
parameter is implicitly moved, and an explicit call to std::move
is not required:
Shape updateShape(Shape&& shape) {
/* ... */
return shape; // Compliant: implicitly moves shape
}
When returning a parameter or variable of rvalue reference type, an implicit move was introduced in C++20 and retroactively applied to previous
standards. As a consequence, the behavior of such return statements is not consistent across compilers and standard versions.
Furthermore, with the C++20 rules, the implicit move is not triggered if the function returns a reference:
Shape&& updateShape(Shape&& shape) {
/* ... */
// C++23: Implicit move, equivalent to `std::move(shape)`
// C++20: No move and ill-formed as Shape&& reference cannot bound to Shape&
return shape;
}
Due to all of the above, this rule does not treat return p
as an exception in C++ standard before C++23, and requires the explicit
move return std::move(p)
.
In contrast to returning local (stack) variables, named return value optimization (NRVO) does not apply to function parameters, so an explicit
std::move
call has no impact on optimizations.
How to fix it
This issue can be resolved in multiple ways:
- Generally,
std::move
can be used to move such arguments;
- For containers, C++20
std::ranges::move
or C++23 std::views::as_rvalue
can be used to move their elements;
- It is also possible to use a range-based for loop to move elements.
We illustrate these solutions in the examples below based on the following definitions.
class Shape {
public:
Shape(Shape const& shape); // Copy constructor
Shape(Shape&& shape); // Move constructor
// More code...
bool isVisible() const;
};
class DrawingStore {
std::vector<Shape> store;
public:
void insertVisibleShape(Shape&& shape);
void insertAllShapes(std::vector<Shape>&& shapes);
void insertAllVisibleShapes(std::vector<Shape>&& shapes);
};
How to move an rvalue parameter
Noncompliant code example
When the parameter represents a single object you want to move, it is not sufficient to use &&
after its type in the parameter
list.
void DrawingStore::insertVisibleShape(Shape&& shape) {
if (shape.isVisible()) {
store.emplace_back(shape); // Noncompliant, call to std::move is expected.
}
}
With the above implementation, the Shape
object appended in store
is created using Shape
's copy
constructor.
Compliant solution
To ensure the object’s content is moved, you have to call std::move()
like this:
void DrawingStore::insertVisibleShape(Shape&& shape) {
if (shape.isVisible()) {
store.emplace_back(std::move(shape)); // Compliant
}
}
With this fix, the move constructor of Shape
is used and the content of the parameter shape
can be transferred
to the newly created object in store
.
How to move elements of a container using for-loops
When you want to transfer the content of multiple objects into another container, it also makes sense to define the parameter as rvalue with
&&
.
Noncompliant code example
While the following code looks fine and compiles, it does actually copy the elements. In fact, shapes
is left unchanged.
void DrawingStore::insertAllShapes(std::vector<Shape>&& shapes) {
for (Shape& s : shapes) {
if (s.isVisible()) {
store.emplace_back(s); // Noncompliant, call to std::move is expected.
}
}
}
Compliant solution
As in the previous example, a call to std::move
is required to fix the implementation:
void DrawingStore::insertAllVisibleShapes(std::vector<Shape>&& shapes) {
for (Shape& s : shapes) {
if (s.isVisible()) {
store.emplace_back(std::move(s)); // Compliant
}
}
}
Writing for (Shape& s : std::move(shapes))
would not fix the issue because this call to std::move
has no effect here.
The call to std::move
has to be on s
, not shapes
.
Notice that in this solution, the for-loop variable s
remains an lvalue reference with a single &
. In C++23, it is
possible to make it a rvalue too, with std::ranges::views::as_rvalue
, making the intent of the code clearer.
void DrawingStore::insertAllVisibleShapes(std::vector<Shape>&& shapes) {
for (Shape&& s : shapes | std::ranges::views::as_rvalue) {
if (s.isVisible()) {
store.emplace_back(std::move(s)); // Compliant
}
}
}
How to move elements of a container using algorithms
Algorithms, especially with C++20 ranges, are often better alternatives to manual for-loops since they abstract away a lot of implementation
details. However, not all of them abstract away the move semantics and attention is required to use them correctly.
Noncompliant code example
For example, std::ranges::copy
performs copies by default:
void DrawingStore::insertAllShapes(std::vector<Shape>&& shapes) {
// Noncompliant: the elements of shapes are not moved.
std::ranges::copy(shapes, std::back_inserter(store));
}
Compliant solution
Here, the solution is fairly simple: std::ranges::copy
can be replaced with std::ranges::move
.
void DrawingStore::insertAllShapes(std::vector<Shape>&& shapes) {
// Compliant: uses "move" instead of "copy".
std::ranges::move(shapes, std::back_inserter(store));
}
Noncompliant code example
However, sometimes std::ranges::move
cannot be used, for example when not all elements should be moved. In this case,
std::ranges::copy_if
looks appropriate but falls short:
void DrawingStore::insertAllVisibleShapes(std::vector<Shape>&& shapes) {
// Noncompliant: the elements of shapes are not moved.
std::ranges::copy_if(
shapes,
std::back_inserter(store),
&Shape::isVisible
);
}
Again, the elements are copied instead of being moved.
Compliant solution
While a solution based on std::make_move_iterator
exists before C++23, it is fairly verbose and error-prone. This time again, C++23
std::ranges::views::as_rvalue
helps writing regular code:
void DrawingStore::insertAllVisibleShapes(std::vector<Shape>&& shapes) {
// Compliant: use as_rvalue to ensure elements are moved.
std::ranges::copy_if(
shapes | std::ranges::views::as_rvalue,
std::back_inserter(store),
&Shape::isVisible
);
}
This solution can be applied to any move-compatible algorithm.