Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Modify rule S5500: mention std::ranges::move and rewrite RSPEC (CPP-5219) #3933

Merged
merged 15 commits into from
May 23, 2024
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion rules/S5500/cfamily/metadata.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"title": "Functions having rvalue reference arguments should \"std::move\" those arguments",
"title": "Function parameters that are rvalue references should be moved",
"type": "CODE_SMELL",
"code": {
"impacts": {
Expand Down
255 changes: 180 additions & 75 deletions rules/S5500/cfamily/rule.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -4,143 +4,248 @@ Rvalue reference arguments allow the efficient transfer of the ownership of obje
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.
This might lead to bugs and performance issues.

This rule does not apply when the argument is a forwarding reference.

=== Exceptions

For the {cpp}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:
[source,cpp]
----
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 {cpp}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 {cpp}20 rules, the implicit move is not triggered if the function
returns a reference:
[source,cpp]
----
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 {cpp} standard before {cpp}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:

// We do not mention std::move_backward or std::ranges::move_backward to keep things simple.
// Those functions are assumed to be less frequently needed.

* Generally, `std::move` can be used to move such arguments;
* For containers, {cpp}23 `std::views::as_rvalue` can be used to move their elements;
* For containers, {cpp}20 `std::ranges::move` or {cpp}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.

This rule does not apply when the argument is a forwarding reference.

We illustrate these solutions in the examples below based on the following definitions.

=== Noncompliant code example

[source,cpp,diff-id=1,diff-type=noncompliant]
[source,cpp]
----
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 insertShape(Shape&& shape) {
if (shape.isVisible()) {
store.emplace_back(shape); // Noncompliant, call to std::move is expected
}
}
void insertVisibleShape(Shape&& shape);
void insertAllShapes(std::vector<Shape>&& shapes);
void insertAllVisibleShapes(std::vector<Shape>&& shapes);
};
----

void insertAllShapes(std::vector<Shape>&& shapes) {
for (auto& s : shapes) {
if (s.isVisible()) {
store.emplace_back(s); // Noncompliant, call to std::move is expected
}
}
=== 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.

[source,cpp,diff-id=1,diff-type=noncompliant]
----
void DrawingStore::insertVisibleShape(Shape&& shape) {
if (shape.isVisible()) {
store.emplace_back(shape); // Noncompliant, call to std::move is expected.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
store.emplace_back(shape); // Noncompliant, call to std::move is expected.
store.emplace_back(shape); // Noncompliant, calls Shape's copy constructor

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wanted to highlight in the code snippet the root cause of the issue as generically as possible.

For example, this would be compliant:

if (shape.isVisible()) { store.emplace_back(shape); }
foo(std::move(shape));

because std::move is called.

In a way, we don't care about the copy/move constructors.

The sentence below is meant to explain what is actually happening and justify why std::move is required. It also serves to connect the two examples.

Does that make sense?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In that case, what would you think of:

void DrawingStore::insertVisibleShape(Shape&& shape) { // Noncompliant, Shape is never moved
  if (shape.isVisible()) {
    store.emplace_back(shape); // Calls Shape's copy constructor.
  }
}

Which would be corrected as:

void DrawingStore::insertVisibleShape(Shape&& shape) { // Compliant
  if (shape.isVisible()) {
    store.emplace_back(std::move(shape)); // Calls Shape's move constructor.
  }
}

To me, the benefits are:

  • As you mention, even if a call to std::move is expected, it could be called in other places
  • I think it more clearly describes what actually happens in the code because:
    • Inline comments avoid having to refer to a place in the code in the next sentence
    • There is a symmetry in the inline comments that highlight the difference

If you still disagree, I'm not going to fight. The RSPEC is correct as it is. I just feel it could be improved.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your idea is not bad, but it would take me a bit of time to adapt and make other sections consistent.

It also has a downside: the diff highlighting will include the first line of the (non)compliant code because of the comment, but only the comment changes. Since the diff is on the whole line (and not on words), this will distract the readers a bit.

So I'll keep the current wording for now. We can always revisit this in the future.

}
};
}
----

With the above implementation, the `Shape` object appended in `store` is created using ``Shape``'s _copy_ constructor.
marco-antognini-sonarsource marked this conversation as resolved.
Show resolved Hide resolved

==== Compliant solution

=== Compliant solution
To ensure the object's content is moved, you have to call `std::move()` like this:

[source,cpp,diff-id=1,diff-type=compliant]
----
class Shape {
// ...
public:
bool isVisible() const;
};
void DrawingStore::insertVisibleShape(Shape&& shape) {
if (shape.isVisible()) {
store.emplace_back(std::move(shape)); // Compliant
marco-antognini-sonarsource marked this conversation as resolved.
Show resolved Hide resolved
}
}
----

class DrawingStore {
std::vector<Shape> store;
public:
void insertShape(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.

[source,cpp,diff-id=2,diff-type=noncompliant]
----
void DrawingStore::insertAllShapes(std::vector<Shape>&& shapes) {
for (auto& s : shapes) {
marco-antognini-sonarsource marked this conversation as resolved.
Show resolved Hide resolved
if (s.isVisible()) {
store.emplace_back(s); // Noncompliant, call to std::move is expected.
}
}
}
----

==== Compliant solution

void insertAllShapes(std::vector<Shape>&& shapes) {
for (auto& s : shapes) {
if (s.isVisible()) {
store.emplace_back(std::move(s)); // Compliant
}
As in the previous example, a call to `std::move` is required to fix the implementation:

[source,cpp,diff-id=2,diff-type=compliant]
----
void DrawingStore::insertAllVisibleShapes(std::vector<Shape>&& shapes) {
for (auto& s : shapes) {
marco-antognini-sonarsource marked this conversation as resolved.
Show resolved Hide resolved
if (s.isVisible()) {
store.emplace_back(std::move(s)); // Compliant
}
}
};
}
----

Alternatively, `insertAllShapes` could also be rewritten like this using {cpp}23:
// We purposely do not go into the details of "move-from" states and the fact that `shapes` has still the same number of elements while some of them are in this "moved-from" state.
marco-antognini-sonarsource marked this conversation as resolved.
Show resolved Hide resolved

Notice that in this solution, the for-loop variable `s` remains an lvalue reference with a single `&`.
loic-joly-sonarsource marked this conversation as resolved.
Show resolved Hide resolved
Furthermore, writing ``++for (auto& s : std::move(shapes))++`` would not fix the issue because this call to `std::move` has no effect here.
marco-antognini-sonarsource marked this conversation as resolved.
Show resolved Hide resolved
The call to `std::move` has to be on `s`, not `shapes`.

Thankfully, {cpp}23 offers a new feature that makes the code more regular: with ``++std::ranges::views::as_rvalue++``, it is now possible to make `s` an rvalue too, making the intent of the code clearer.

// We do not use the shorter form std::views::as_rvalue because libstdc++ does not support it yet.

[source,cpp]
----
void insertAllShapes(std::vector<Shape>&& shapes) {
std::ranges::copy_if(
shapes | std::views::as_rvalue,
std::back_inserter(store),
&Shape::isVisible
);

// Alternatively:
for (auto&& s : shapes | std::views::as_rvalue) {
if (s.isVisible()) {
store.emplace_back(std::move(s)); // Compliant
}
void DrawingStore::insertAllVisibleShapes(std::vector<Shape>&& shapes) {
for (auto&& s : shapes | std::ranges::views::as_rvalue) {
marco-antognini-sonarsource marked this conversation as resolved.
Show resolved Hide resolved
if (s.isVisible()) {
store.emplace_back(std::move(s)); // Compliant
loic-joly-sonarsource marked this conversation as resolved.
Show resolved Hide resolved
}
}
}
----

== Exceptions
=== How to move elements of a container using algorithms

For the {cpp}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:
[source,cpp]
Algorithms, especially with {cpp}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:

[source,cpp,diff-id=3,diff-type=noncompliant]
----
Shape updateShape(Shape&& shape) {
/* ... */
return shape; // Compliant: implicitly moves shape
void DrawingStore::insertAllShapes(std::vector<Shape>&& shapes) {
// Noncompliant: the elements of shapes are not moved.
std::ranges::copy(shapes, std::back_inserter(store));
}
----

When returning a parameter or variable of rvalue reference type, an implicit move
was introduced in {cpp}20 and retroactively applied to previous standards.
As a consequence, the behavior of such return statements is not consistent across compilers
and standard versions.
==== Compliant solution

Furthermore, with the {cpp}20 rules, the implicit move is not triggered if the function
returns a reference:
[source,cpp]
Here, the solution is fairly simple: `std::ranges::copy` can be replaced with `std::ranges::move`.

[source,cpp,diff-id=3,diff-type=compliant]
----
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;
void DrawingStore::insertAllShapes(std::vector<Shape>&& shapes) {
// Compliant: use "move" instead of "copy".
marco-antognini-sonarsource marked this conversation as resolved.
Show resolved Hide resolved
std::ranges::move(shapes, std::back_inserter(store));
}
----

Due to all of the above, this rule does not treat `return p` as an exception in {cpp} standard prior to {cpp}23,
and requires the explicit move `return std::move(p)`.
==== Noncompliant code example

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.
However, sometimes `std::ranges::move` cannot be used, for example when the transfer of container should be conditional.
marco-antognini-sonarsource marked this conversation as resolved.
Show resolved Hide resolved
In this case, `std::ranges::copy_if` looks appropriate but falls short:

[source,cpp,diff-id=4,diff-type=noncompliant]
----
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 copies instead of moved.
marco-antognini-sonarsource marked this conversation as resolved.
Show resolved Hide resolved

==== Compliant solution

While a solution using {cpp}20 exists, it is fairly verbose and error-prone.
loic-joly-sonarsource marked this conversation as resolved.
Show resolved Hide resolved
This time again, {cpp}23 ``++std::ranges::views::as_rvalue++`` helps writing regular code:

[source,cpp,diff-id=4,diff-type=compliant]
----
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.

== Resources

=== Documentation

// Not linking to the _backward versions, to the std::move(start, end, result) overload,
// or std::make_move_iterator function to keep the number of links manageable.

* {cpp} reference - https://en.cppreference.com/w/cpp/utility/move[`std::move`]
* {cpp} reference - https://en.cppreference.com/w/cpp/algorithm/ranges/move[`std::ranges::move`]
* {cpp} reference - https://en.cppreference.com/w/cpp/ranges/as_rvalue_view[`std::ranges::views::as_rvalue`]
* {cpp} reference - https://en.cppreference.com/w/cpp/language/copy_elision[Copy elision]

=== External coding guidelines

* {cpp} Core Guidelines - https://github.com/isocpp/CppCoreGuidelines/blob/e49158a/CppCoreGuidelines.md#f18-for-will-move-from-parameters-pass-by-x-and-stdmove-the-parameter[F.18: For "will-move-from" parameters, pass by `X&&` and `std::move` the parameter]
* {cpp} reference - https://en.cppreference.com/w/cpp/language/copy_elision[Copy elision]

=== Related rules

Expand Down