6

Consider the following class Buffer, which contains an std::vector object:

#include <vector>
#include <cstddef>

class Buffer {
   std::vector<std::byte> buf_;
protected:
   Buffer(std::byte val): buf_(1024, val) {}
};

Now, consider the function make_zeroed_buffer() below. The class BufferBuilder is a local class that publicly derives from Buffer. Its purpose is to create Buffer objects.

Buffer make_zeroed_buffer() {
   struct BufferBuilder: Buffer {
      BufferBuilder(): Buffer(std::byte{0}) {}
   };

   BufferBuilder buffer;

   // ...

   return buffer;
}

If no copy elision takes place, is the buffer object above guaranteed to be moved from?

My reasoning is the following:

  1. The expression buffer in the return statement is an lvalue. Since it is a local object that is not going to be used anymore, the compiler casts it into an rvalue.
  2. The buffer object is of type BufferBuilder. Buffer is a public base class of BufferBuilder, so this BufferBuilder object is implicitly converted into a Buffer object.
  3. This conversion, in turn, implies an implicit reference-to-derived to a reference-to-base conversion (i.e., a reference to BufferBuilder to a reference to Buffer). That reference to BufferBuilder is an rvalue reference (see 1.), which turns into an rvalue reference to Buffer.
  4. The rvalue reference to Buffer matches Buffer's move constructor, which is used to construct the Buffer object that make_zeroed_buffer() returns by value. As a result, the return value is constructed by moving from the Buffer part of the object buffer.
Christopher Oezbek
  • 23,994
  • 6
  • 61
  • 85
JFMR
  • 23,265
  • 4
  • 52
  • 76
  • 2
    *If no copy elision takes place* What makes you think that this condition will ever be satisfied? From my recollection, when you explicitly `return std::move(buffer);` the compiler (clang) will issue a warning about surpressing copy elision. – Walter Jul 14 '19 at 15:07
  • @Walter What makes you think then that copy elision will always be satisfied? – JFMR Jul 14 '19 at 15:13
  • If I remember well, in this very particular case you have to use `return std::move(buffer)` – BiagioF Jul 14 '19 at 15:17
  • @BiagioFesta can you please elaborate? – JFMR Jul 14 '19 at 15:25
  • 2
    No copy elision. The next statement is violated: In a return statement, when the operand is a prvalue of the same class type (ignoring cv-qualification) as the function return type – 273K Jul 14 '19 at 15:35
  • 1
    *The buffer object is of type BufferBuilder. Buffer is a public base class of BufferBuilder, so this BufferBuilder object is implicitly converted into a Buffer object.* It is not true. The BufferBuilder object is copied implicitly to a Builder object. You confused with pointers to a derived class. – 273K Jul 14 '19 at 15:44
  • @S.M. Doesn't it apply to references as well? – JFMR Jul 14 '19 at 15:45
  • Yes, it does. Your code does not have any reference. Except the implicit Buffer's copy constructor. – 273K Jul 14 '19 at 15:49
  • According to the common programming patterns the BufferBuilder must not derive the being built class. – 273K Jul 14 '19 at 15:55
  • Also, in your 'step 3', as of C++20, that **never** happens. There is no situation in which the compiler will notice that a variable is no longer used and implicitly convert an lvalue into an rvalue. One must always explicitly state this desire by using `::std::move`. It could be added as a 'feature'. But that's one of those features that I don't think has much benefit, especially as compared to the added complexity it would introduce. – Omnifarious Jul 14 '19 at 17:11

2 Answers2

7

RVO Optimisation

If no copy elision takes place [...]

Actually, copy elision will not take place (without if).

From C++ standard class.copy.elision#1:

is permitted in the following circumstances [...]:

-- in a return statement in a function with a class return type, when the expression is the name of a non-volatile automatic object ([...]) with the same type (ignoring cv-qualification) as the function return type [...]

Technically, when you return a derived class and a slicing operation takes place, the RVO cannot be applied.

Technically RVO works constructing the local object on the returning space on the stack frame.

|--------------|
| local vars   |
|--------------|
| return addr  |
|--------------|
| return obj   |
|--------------|

Generally, a derived class can have a different memory layout than its parent (different size, alignments, ...). So there is no guarantee the local object (derived) can be constructed in the place reserved for the returned object (parent).


Implicit move

Now, what about implicit move?

is the buffer object above guaranteed to be moved from???

In short: no. On the contrary, it is guaranteed the object will be copied!

In this particular case implicit move will not be performed because of slicing.

In short, this happens because the overload resolution fails. It tries to match against the move-constructor (Buffer::Buffer(Buffer&&)) whereas you have a BufferBuild object). So it fallbacks on the copy constructor.

From C++ standard class.copy.elision#3:

[...] if the type of the first parameter of the selected constructor or the return_­value overload is not an rvalue reference to the object's type (possibly cv-qualified), overload resolution is performed again, considering the object as an lvalue.

Therefore, since the first overload resolution fails (as I have said above), the expression will be treated as an lvalue (and not an rvalue), inhibiting the move.

An interesting talk by Arthur O'Dwyer specifically refers to this case. Youtube Video.


Additional Note

On clang, you can pass the flag -Wmove in order to detect this kind of problems. Indeed for your code:

local variable 'buffer' will be copied despite being returned by name [-Wreturn-std-move]

   return buffer;

          ^~~~~~

<source>:20:11: note: call 'std::move' explicitly to avoid copying

   return buffer;

clang directly suggests you to use std::move on the return expression.

Community
  • 1
  • 1
BiagioF
  • 9,368
  • 2
  • 26
  • 50
  • 1
    In standardese, the implicit move is disallowed because "the type of the first parameter of the selected constructor (`Buffer&&`) is not an rvalue reference to the object's type (`BufferBuilder&&`)" http://eel.is/c++draft/class.copy.elision#3.sentence-2 – Oktalist Jul 14 '19 at 16:08
  • Yeah, that what I've written (with other words). But thank you, maybe I can rephrase better and add a reference to the standard. – BiagioF Jul 14 '19 at 16:10
  • You phrased it fine, I just wanted to show the "proof". – Oktalist Jul 14 '19 at 16:11
  • If my reading of the spec is correct, this modification of the local class should make the compiler move it: `struct BufferBuilder { BufferBuilder():buffer(std::byte{0}) {} Buffer buffer; operator Buffer() && { return (Buffer&&)buffer; } };` Because there is no "selected constructor" anymore. (EDIT: needs a public constructor in Buffer :/) – Johannes Schaub - litb Jul 14 '19 at 18:28
  • Clang disagrees with my reading. GCC agrees. Clang says: ":16:7: note: candidate function not viable: no known conversion from 'BufferBuilder' to 'BufferBuilder' for object argument operator Buffer() && { return (Buffer&&)buffer; }". I think that this is a bug in Clang. – Johannes Schaub - litb Jul 14 '19 at 18:33
  • 1
    Does GCC have a flag equivalent to `-Wmove`? – Eric Jul 15 '19 at 06:27
  • @Eric No, it doesn't as far as I know – BiagioF Jul 15 '19 at 10:08
-1

The object buffer from make_zeroed_buffer() will destroyed after will be made its copy with the help of Buffers' copy constructor for return value.

zrk725945
  • 26
  • 2