Overview
When using STL containers such as lists, vectors, maps, a lot of temporary objects are created and destroyed during the life time of an application.
Consider the following class mystring which is derived from std::string. The string is internally stored in the char *buffer member of std::string. This buffers gets allocated and deallocated several times during its life time inside constructors, assignment operators. This can be clearly seen in the console output as the overriding functions of mystring prints to console whenever they are called.
struct mystring:std::string { //defined in basic_string class; contains underlying string //char *buffer; mystring() { cout << "default constructor" << endl; } mystring(const char* s):std::string(s) { cout << "memberwise constructor\t: " << s << endl; } mystring(const mystring& s) :std::string(s) { cout << "copy constructor\t: " << s << endl; } mystring& operator=(const mystring& s) { cout << "operator=\t\t: " << s << endl; dynamic_cast<string*>(this)->operator=(s); return *this; } };//Examples
//swap mystring g{"Old Fridge"}, w{"New Fridge"};
mystring t = g; g = w; w = t;
As in this example 17, two instances of mystring are swapped. From the console output, it can be seen that the underlying string buffer is potentially allocated in the copy constructor of variable t and assignment operators. It's potential because string does optimization; if the buffers are big enough to copy, buffers are not reallocated.
memberwise constructor <----------- mystring g{"Old Fridge"}
memberwise constructor <----------- mystring w{"New Fridge"}
copy constructor <----------- mystring t = g;
operator= <----------- g = w;
operator= <----------- w = t;
Details
To overcome this, rvalue reference and move semantics are introduced.
rvalue reference
In the assignment statement below, i is referred as lvalue, shorthand for lhs value or left hand side value and 10 is referred as rvalue, shorthand for rhs value or right hand side value. rvalue is mostly associated with temporary objects.
int i;
i = 10;
An lvalue reference is formed by placing an & after some type.
int x{};
const int y = 0; int &ref1 = x; // OK int &ref2 = y; // Error int &ref3 = 5; // Error const int &ref4 = x; // OK const int &ref5 = y; // OK const int &ref6 = 5; // OK
An rvalue reference is formed by placing an && after some type.
int x{};
const int y = 0; int &&ref7 = x; // Error int &&ref8 = y; // Error int &&ref9 = 5; // OK const int &&ref10 = x; // Error const int &&ref11 = y; // Error const int &&ref12 = 5; // OK
lvalue references can bind to lvalues and rvalues.
An rvalue reference is similar to a lvalue reference except that it can bind to a only temporary value (a rvalue), but not to a lvalue.
struct A {} a;
A& a_ref3 = A(); // Error!
A& a_ref4 = a; // Ok
A&& a_ref5 = A(); // Ok
A&& a_ref6 = a; // Error!
move
move semantics takes advantage of the new rvalue reference to optimize temporary object creation. In order to use move semantics, classes are expected to support rvalue copy constructor and rvalue assignment operator. In these functions, expensive resources such as heap memory are stripped off the source object and assigned to the target object instead of reallocation. After move operation, source object may not be used.
The move operation itself involves just changing the lavalue type to rvalue type.
After adding the rvalue constructor and rvalue assignment operator, the optimization can be seen in the example 18 and its console output.
The difference is the the rvalue copy constructor and rvalue assignment operators are called that reuses the string object without reallocation.
mystring(mystring&& s) noexcept :std::string(s) { cout << "move constructor:" << s << endl; } mystring& operator=(mystring&& s) noexcept { cout << "move operator=:" << s << endl; dynamic_cast<string*>(this)->operator=(s); return *this; }//Examples
//swapmystring g{"Old Fridge"}, w{"New Fridge"};mystring t = move(g); g = move(w); w = move(t);
In case of swap, std::move() is called to use rvalue assignment operator instead of lvalue assignment operator.
memberwise constructor <----------- mystring g{"Old Fridge"} memberwise constructor <----------- mystring w{"New Fridge"} move constructor <----------- mystring t = move(g); move operator= <----------- g = move(w); move operator= <----------- w = move(t);
Perfect forwarding
The rvalue semantics enable passing the arguments without ambiguity.
As shown in an example below, the overloaded print() accepts three different parameters.
print(string& message);
print(const string& message);
print(string&& message);
Without rvalue semantics as shown below, it would be impossible to create a factory method that calls the correct overloaded print() method by forwarding the exact arguments as shown below.
template<class T> void factoryprint(T&& message) { print(forward<T>(message)); }
//Example string message="hello, world!"; const string message2="hello, world!"; factoryprint(message); //calls print(string& message) factoryprint(message2) //calls print(const string& message) factoryprint(move(message)); //calls print(string&& message)
The template function std::forward enables perfect forwarding: the ability to pass arguments while maintaining their value category (e.g. lvalues stay as lvalues, temporaries are forwarded as rvalues).
Forwarding references allow a reference to bind to either an lvalue or rvalue depending on the type.
It follows the rules of reference collapsing as shown below:
type | binding | result |
---|---|---|
T& | & | T& |
T& | && | T& |
T&& | & | T& |
T&& | && | T&& |
universal reference
Before C++11, universal reference was used to bind to both rvalue and lvalue. An universal reference is formed by making const reference of some type as shown below.
const int& i=45; // universal reference
int j=5; const int& k=j; // universal reference
An universal reference can also be used to pass both rvalue and lvalue as function parameters.
void print(const int& val); int i=10; print(10); print (i);
No comments:
Post a Comment