Pages

What is Rvalue reference good for

C++0x gives us a way of working not only with Lvalue reference (the "classic" reference provided by C++) but also with Rvalue reference. This help us to write more performing code. Let's see how.

Here is a simple class wrapping a resource that we should think as expensive to create and copy:

class Something
{
private:
char* s_;
void init(const char* s)
{
std::cout << "expensive operation" << std::endl;
s_ = new char[strlen(s)+1];
strcpy(s_, s);
}
public:
Something(const char* s = nullptr)
{
std::cout << "ctor for " << (s ? s : "nullptr") << std::endl;
if(s)
init(s);
else
s_ = nullptr;
}

Something(const Something& rhs)
{
std::cout << "copy ctor" << std::endl;
init(rhs.s_);
}

Something& operator=(const Something& rhs)
{
std::cout << "assignment operator" << std::endl;
if(this == &rhs)
return *this;

delete s_;
init(rhs.s_);
return *this;
}

const char* get() { return s_ ? s_ : ""; }

~Something()
{
std::cout << "dtor for " << (s_ ? s_ : "nullptr") << std::endl;
delete s_;
}
};

It is often useful to provide a factory method. Let see a couple of such functions:

Something createSomething(const char* st)
{
std::cout << "A local object that could be easily optimized" << std::endl;
return Something(st); // 1.
}

Something createSomething2(const char* st)
{
std::cout << "A local object that can't be easily optimized" << std::endl;
Something s(st); // 2.

std::cout << "Some job required between object creation and return" << std::endl;
return s;
}

1. If we can put the creation of the object in the return statement, we usually could rely on the compiler to perform all the possible optimization. Theoretically speaking we should create a temporary object here, and copy it to its natural destination in the caller code, but almost any compiler is smart enough to remove this copy in the produced code.
2. If object creation and its return to the call are not part of the same instruction, the compiler usually has no way of performing such an aggressive optimization.

We usually call that function in this way:

Something s4 = createSomething("wow");
Something s5 = createSomething2("mow");

Having a look to the log produced by the first call:

A local object that could be easily optimized
ctor for wow
expensive operation

We see that the compiler actually removed the unnecessary creation/deletion of unnecessary temporary objects, minimizing the operation cost.

For the second call we don't have such a luck:

A local object that can't be easily optimized
ctor for mow
expensive operation
Some job required between object creation and return
copy ctor
expensive operation
dtor for mow

As we see, there is an unnecessary creation/deletion of a temporary object that implies a call too much to the operation marked as expensive.

Let's use the new Rvalue reference to improve the Something class:

Something(Something&& rhs) // 1.
{
std::cout << "Move copy ctor for " << rhs.s_ << std::endl;

s_ = rhs.s_;
rhs.s_ = nullptr;
}

Something& operator=(Something&& rhs) // 2.
{
std::cout << "Move assignment for " << rhs.s_ << std::endl;

delete s_;
s_ = rhs.s_;
rhs.s_ = nullptr;

return *this;
}

1. A copy ctor that relies on the fact that the passed object is a temporary. We don't need to create a new resource copy of the passed one, we can simply steal it!
2. Same for the assignment operator.

If we use our improved Something class definition, we have this log:

A local object that can't be easily optimized
ctor for mow
expensive operation
Some job required between object creation and return
Move copy ctor for mow
dtor for nullptr

We still have a temporary object, but we take advantage of knowing about its nature, and we spare the not necessary call to the expensive operation.

No comments:

Post a Comment