I wrote my own string type (Str) to demonstrate the basic constructors, destructors and assignment operators; and, I can see them all execute in C++17 except the move constructor.
Apparently, the move constructor is not used much anymore because of Return Value Optimization (RVO).
Is the move constructor only called in response to explicitly calling std::move?
When else might it be called?
Is it mostly obsolete because of RVO?
Here's my Str type:
struct Str {
Str(): p(nullptr) {}
Str(const char* s) { cout << "cvctor \"" << s << "\"\n"; copy(s); }
Str(const Str& s): p(nullptr) { cout << "cpctor deep\""<<s.p<<"\"\n"; copy(s.p); }
Str( Str&& s) { cout << "mvctr shallow \"" << s.p << "\"\n"; p = s.p; s.p=nullptr; }
const Str& operator=(const Str& s) { cout << "op=\""<<s.p<<"\"\n"; copy(s.p); return *this; }
const Str& operator=( Str&& s) { cout << "op= shallow \""<<s.p<<"\"\n"; p=s.p; s.p=nullptr; return *this; }
~Str(){ if ( p != nullptr ) { cout << "dtor \"" << p << "\"\n"; delete [] p; } }
private:
char* p = nullptr;
char* copy(const char* s)
};
Return value optimization is not the only point where the move constructor is used. Every time you want to construct a value of some type from an rvalue is a point where the move constructor is used.
You basically ask two questions in one. Let's start with
Is the move constructor only called in response to explicitly calling std::move?
The move constructor and std::move are tangentially related but in essence very separate. The move constructor is called every time you initialize a variable from an rvalue of the same type. On the other hand std::move makes it possible to explicitly get from a so-called lvalue to such an rvalue but it is not the only way.
template<typename T>
void foo(T&& value) { // A so-called universal reference
T other_value = std::forward<T>(value);
}
foo( string{"some string"} ); // other_value is move initialized
You see, std::forward is another way to obtain an rvalue. Actually, "some string" also results in an rvalue in the above code, of type const char*.
Time for an intermezzo. If you hear rvalue you might be tempted to think about && which is an rvalue-reference. This is subtly different. The problem is that giving a name to anything makes it an lvalue. So the following code:
foo(string&& value) {
T other_value = value;
}
foo( "some_string" ); // other_value is STILL copy initialized
foo(string&& value) {
T other_value = std::move(value);
}
foo( "some_string" ); // other_value is now properly move initialized
The correct way to think about && is that such a reference can be initialized with an rvalue but it is itself not always such an rvalue. For more information see also here
Is it mostly obsolete because of RVO?
Two notable examples where the move constructor is prominently often used besides RVO come to mind
moving into method arguments
void foo(string value);
// Somewhere else
string& some_string = get_me_a_string();
foo( ::std::move(some_string) ); // Uses move constructor to initialize value
some_string.clear(); // Most probably a no-op
// Doing this leaves an empty some_string
Note that in the example above, the fact that some_string is a reference does not matter whether move construction is used. It is a reference to make it clear that RVO might not always be possible. In this case, after some_string has been moved from, will be left in an unspecified, but valid state which is a fancy way to say that no undefined behaviour will occur and the reference is still valid.
moving into fields
class FooBar {
string fooField;
//Constructor
FooBar( string bar )
: fooField( ::std::move(bar) ) // Uses move constructor to initialize fooField
{ }
}
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