To properly initialize class data members or base classes in a move constructor's initializer list, it's important to ensure that the object hierarchy is being move-initialized, rather than copy-initialized. An rvalue reference parameter is itself an lvalue and steps must be taken to preserve move semantics. See the examples below(1).
The lvalue can be turned into an xvalue(same as glvalue & rvalue) by passing it to std::move
and it can be used to perform initialization.
But for trivially copyable objects we shouldn't use std::move
, as it can negatively impact performance.
struct BaseClass {
BaseClass() = default;
BaseClass(const BaseClass& other);
BaseClass(BaseClass&& other);
};
struct DerivedClass : BaseClass {
DerivedClass() = default;
DerivedClass(const DerivedClass& other) : BaseClass(other) {}
// (1) In the below line, the rvalue reference parameter
// is treated as lvalue when passed to BaseClass().
// Hence the same call will result in invocation of
// copy-constructor.
DerivedClass(DerivedClass&& other) : BaseClass(other) {}
};
struct EnclosingClass {
BaseClass B;
// The member variable is copy-initialised rather then move initilised
BaseClass(BaseClass& i) : B(i);
};
struct BaseClass {
BaseClass() = default;
BaseClass(const BaseClass &other);
BaseClass(BaseClass &&other);
};
struct DerivedClass : BaseClass {
DerivedClass() = default;
DerivedClass(const DerivedClass &other) : BaseClass(other) {}
// (1) In the below line, the rvalue reference parameter is explicitly
// converted to rvalue using std::move hence resulting in invocation of move
// constructor.
DerivedClass(DerivedClass &&other) : BaseClass(std::move(other)) {}
};
struct EnclosingClass {
BaseClass B;
// The member variable is copy-initialised rather then move initilised
BaseClass(BaseClass& i) : B(i);
};