PHP Advanced C++11 - RValue References & Move Semantics

AceInfinity

Emeritus, Contributor
Joined
Feb 21, 2012
Posts
1,728
Location
Canada
Firstly, a brief abstract of what an RValue reference is most likely needed here, since it is fundamental to understanding any of the deeper and more complex concepts associated with these new features which are part of C++11.

An RValue is an expression in which is either a PrValue or an XValue. If an overload of something provides an RValue reference and another takes an LValue reference to a const parameter, RValues *bind* to the RValue reference overload, invoking the move constructor; move semantics.

PrValue - An expression that identifies a temp object or is a value which is not associated with any object. Literals such as 99, true, false, 'a', nullptr, are of this definition, including lambda expressions, and function calls for instance.

XValue - An expression that denotes an expiring object (nameless temp object, named object within a scope). Casts to RValue references for instance are XValues (&&), among others.

Code:
int x = 1; // x is an lvalue
int &xref = a; // xref is an lvalue reference

So let's say we have two pre-defined functions:
Code:
void print(int &x);
void print(int &&x);

If we call print(x), the first overload will be invoked, but if we call print(1), the second overload will be called. As of C++11 there are 4 reference overloads that can be created:

Code:
function(T &);
function(const T &);
function(T &&);
function(const T &&);

If you were to create another:
Code:
function(T);

This would be ambiguous as the compiler would not know whether to call either function(T) or function(T &), otherwise, function(T) or function(T &&).

Imagine a function which returns a temp object. Using assignment, this temp variable is usually expected to be assigned to a destination through multiple transfers of copying that data.

C++11 completely redefines the idea of passing by copy/value, because not under all circumstances is this required to pass a copy of the object, subsequently calling the copy constructor. This can be achieved through move constructors; move semantics.

The compiler also may optimize this for you, but it's not always guaranteed to work, and it depends on your compiler support for it as well; copy elision, and RVO (return value optimization).

If you have an object, and you assign it to another object which should in theory be created by a copy of some temp object, what if you could just use that temp object and assign it directly to the lvalue? This is indeed possible.

Lets look at some example I put together:
Code:
#include <iostream>
#include <utility>
#include <vector>

template <class T>
class array_wrapper
{
public:
  array_wrapper()
    : arr(new T[_default_size])
    , _size(_default_size) { }

  array_wrapper(const size_t size)
    : arr(new T[size])
    , _size(size) { }

  array_wrapper(T &rhs)
  {
    for (size_t i = 0; i < _size; ++i)
      arr[i] = rhs[i];
  }

  array_wrapper(T &&rhs) : _size(rhs._size)
  {
    this->arr = rhs.arr;
    rhs.arr = nullptr;
  }

  ~array_wrapper() { delete [] arr; }

  size_t get_size() const { return _size; }
  T *arr;
private:
  const static size_t _default_size = 64;
  const size_t _size;
};

template <class T> array_wrapper<T> create_object() { return array_wrapper<T>(); }
template <class T> array_wrapper<T> create_object(size_t size) { return array_wrapper<T>(size); }
template <class T> array_wrapper<T> get_object(array_wrapper<T> obj) { return obj; }

int main()
{
  array_wrapper<int> obj1 = create_object<int>(32); // int[32]
  for (size_t i = 0; i < obj1.get_size(); ++i)
    obj1.arr[i] = i * 2;

  array_wrapper<int> &&obj2 = get_object<int>(std::move(obj1));
  // array_wrapper<int> obj2 = obj1;

  std::cout << "obj1 [" << &obj1 << "] " << "obj1.arr = " << obj1.arr <<
            ", obj1.size = " << obj1.get_size() << std::endl;

  std::cout << "obj2 [" << &obj2 << "] " << "obj2.arr = " << obj2.arr <<
            ", obj2.size = " << obj2.get_size() << std::endl;
}

Some of the code is not the way you would write production code in most circumstances, but the idea behind this code should be obvious when compiled and ran.

Here, instead of the embodied data being copied through the copy constructor, which you might assume uses an iteration and copies values over into a newly allocated block of memory, we steal the pointer, such that no new allocation is necessary, no copying of the data. Then we set the other object's pointer to 0 because when that destructor is called, it won't free up the memory and junk the data that exists at the address we set our pointer to in the other object.

NOTE: In C++11 move semantics are implemented for all STL containers by default. This means that if you are writing C++11 code from C++03 as a transition, you won't have to re-write any of your code and it will automatically showcase performance benefits without having to change a thing, assuming you enjoy using the STL.

std::move() is defined similarly as:
Code:
template< typename T > 
typename remove_reference<T>::type&&  move(T&& t) noexcept {
  return static_cast<typename remove_reference<T>::type&&>(t);
};

Now this looks pretty intimidating at first, but essentially what this does is it will treat an lvalue or rvalue as an rvalue reference.

Imagine the infamous swap() method:
Code:
int tmp = a;
a = b;
b = tmp;

Here we have 2 copies of a, and when we set a to b, we have 2 copies of b, then setting b to tmp (which was the value of a), we then end up with 2 copies of a.

Something a bit revised:
Code:
template <class T> inline void swap(T &a, T &b)
{
  T tmp(std::move(a));
  a = std::move(b);
  b = std::move(tmp);
}

And this solves a bit of the redundant, or at least unnecessary copies by utilizing move semantics through std::move().

If anybody is up for it, here's a good article: Rvalue reference pitfalls, and an update Rvalue reference pitfalls, an update. It is very convoluted at first, and a good demonstration of how complex C++11 has made the standard of the language, yet if you have good background knowledge of lvalues and rvalues, you should have no problem understanding these principles.

A very well known topic, also related to this, is perfect forwarding.
 
Back
Top