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:
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.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.BufferBuilder
to a reference to Buffer
). That reference to BufferBuilder
is an rvalue reference (see 1.), which turns into an rvalue reference to Buffer
.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
.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.
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.
Every constructor in the inheritance hierarchy gets called, in the order Base -> Derived. Destructors get called in the reverse order.
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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With