Grapes Grapes - 16 days ago 4
C++ Question

Correct usage of rvalue references as parameters

Let's take the following method as an example:

void Asset::Load( const std::string& Path )
{
// complicated method....
}


General use of this method would be as follows:

Asset ExampleAsset;
ExampleAsset.Load("image0.png");


Since we know most of the time the Path is a temporary rvalue, does it make sense to add an Rvalue version of this method? And if so, is this a correct implementation;

void Asset::Load( const std::string& Path )
{
// complicated method....
}
void Asset::Load( std::string&& Path )
{
Load(Path); // call the above method
}


Is this a correct approach to writing rvalue versions of methods?

Answer

For your particular case, the second overload is useless.

With the original code, which has just one overload for Load, this function is called for lvalues and rvalues.

With the new code, the first overload is called for lvalues and the second is called for rvalues. However, the second overload calls the first one. At the end, the effect of calling one or the other implies that the same operation (whatever the first overload does) will be performed.

Therefore, the effects of the original code and the new code are the same but the first code is just simpler.

Deciding whether a function must take an argument by value, lvalue reference or rvalue reference depends very much on what it does. You should provide an overload taking rvalue references when you want to move the passed argument. There are several good references on move semantincs out there, so I won't cover it here.

Bonus:

To help me make my point consider this simple probe class:

struct probe {
    probe(const char*  ) { std::cout << "ctr " << std::endl; }
    probe(const probe& ) { std::cout << "copy" << std::endl; }
    probe(probe&&      ) { std::cout << "move" << std::endl; }
};

Now consider this function:

void f(const probe& p) {
    probe q(p);
    // use q;
}

Calling f("foo"); produces the following output:

ctr
copy

No surprises here: we create a temporary probe passing the const char* "foo". Hence the first output line. Then, this temporary is bound to p and a copy q of p is created inside f. Hence the second output line.

Now, consider taking p by value, that is, change f to:

void f(probe p) {
    // use p;
}

The output of f("foo"); is now

ctr

Some will be surprised that in this case: there's no copy! In general, if you take an argument by reference and copy it inside your function, then it's better to take the argument by value. In this case, instead of creating a temporary and copying it, the compiler can construct the argument (p in this case) direct from the input ("foo"). For more information, see Want Speed? Pass by Value. by Dave Abrahams.

There are two notable exceptions to this guideline: constructors and assignment operators.

Consider this class:

struct foo {
    probe p;
    foo(const probe& q) : p(q) { }
};

The constructor takes a probe by const reference and then copy it to p. In this case, following the guideline above doesn't bring any performance improvement and probe's copy constructor will be called anyway. However, taking q by value might create an overload resolution issue similar to the one with assignment operator that I shall cover now.

Suppose that our class probe has a non-throwing swap method. Then the suggested implementation of its assignment operator (thinking in C++03 terms for the time being) is

probe& operator =(const probe& other) {
    probe tmp(other);
    swap(tmp);
    return *this;
}

Then, according to the guideline above, it's better to write it like this

probe& operator =(probe tmp) {
    swap(tmp);
    return *this;
}

Now enter C++11 with rvalue references and move semantics. You decided to add a move assignment operator:

probe& operator =(probe&&);

Now calling the assignment operator on a temporary creates an ambiguity because both overloads are viable and none is preferred over the other. To resolve this issue, use the original implementation of the assignment operator (taking the argument by const reference).

Actually, this issue is not particular to constructors and assignment operators and might happen with any function. (It's more likely that you will experience it with constructors and assignment operators though.) For instance, calling g("foo"); when g has the following two overloads raises the ambiguity:

void g(probe);
void g(probe&&);