Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Move semantics in derived-to-base class conversions

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.
like image 849
ネロク・ゴ Avatar asked Jul 14 '19 14:07

ネロク・ゴ


People also ask

Which constructor is called first that of the derived class or the base class?

C++ For multiple inheritance order of constructor call is, the base class's constructors are called in the order of inheritance and then the derived class's constructor.

Why derived class destructor is called first?

The derived class must be constructed after the base class so that the derived class constructor can refer to base class data. For the same reason, the derived class destructor must run before the base class destructor. It's very logical: we construct from the inside out, and destroy from the outside in.

Is the constructor of base class called?

Every constructor in the inheritance hierarchy gets called, in the order Base -> Derived. Destructors get called in the reverse order.

What is a derived class constructor?

Constructors in Derived Class in C++If the class “A” is written before class “B” then the constructor of class “A” will be executed first. But if the class “B” is written before class “A” then the constructor of class “B” will be executed first.


1 Answers

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.

like image 53
BiagioF Avatar answered Sep 28 '22 02:09

BiagioF