Should I return an rvalue reference parameter by rvalue reference?
Asked Answered
H

4

32

I have a function which modifies std::string& lvalue references in-place, returning a reference to the input parameter:

std::string& transform(std::string& input)
{
    // transform the input string
    ...

    return input;
}

I have a helper function which allows the same inline transformations to be performed on rvalue references:

std::string&& transform(std::string&& input)
{
    return std::move(transform(input)); // calls the lvalue reference version
}

Notice that it returns an rvalue reference.

I have read several questions on SO relating to returning rvalue references (here and here for example), and have come to the conclusion that this is bad practice.

From what I have read, it seems the consensus is that since return values are rvalues, plus taking into account the RVO, just returning by value would be as efficient:

std::string transform(std::string&& input)
{
    return transform(input); // calls the lvalue reference version
}

However, I have also read that returning function parameters prevents the RVO optimisation (for example here and here)

This leads me to believe a copy would happen from the std::string& return value of the lvalue reference version of transform(...) into the std::string return value.

Is that correct?

Is it better to keep my std::string&& transform(...) version?

Heikeheil answered 7/5, 2015 at 7:2 Comment(4)
As a side note, the original function that accepts and returns ordinary &s is pretty nasty - it mutates the object passed to it, but its disguised to look like a pure function. It's a recipe for misunderstanding. This is possibly what makes it difficult to figure out the "right" way to make an rvalue variant of it.Irmine
What's the point of returning something the user already has ? It's not like you're gonna chain call transform, is it ?Chrome
@Drax, what about std::cout << foo(transform(get_str()));?Heikeheil
@SteveLorimer Fair enough :) Not sure it justifies the whole interface design though, also i would expect the function to copy the string if it returns something, acting on a reference and returning it is not common. But it seems valid enough :)Chrome
E
14

There's no right answer, but returning by value is safer.

I have read several questions on SO relating to returning rvalue references, and have come to the conclusion that this is bad practice.

Returning a reference to a parameter foists a contract upon the caller that either

  1. The parameter cannot be a temporary (which is just what rvalue references represent), or
  2. The return value won't be retained past the the next semicolon in the caller's context (when temporaries get destroyed).

If the caller passes a temporary and tries to save the result, they get a dangling reference.

From what I have read, it seems the consensus is that since return values are rvalues, plus taking into account the RVO, just returning by value would be as efficient:

Returning by value adds a move-construction operation. The cost of this is usually proportional to the size of the object. Whereas returning by reference only requires the machine to ensure that one address is in a register, returning by value requires zeroing a couple pointers in the parameter std::string and putting their values in a new std::string to be returned.

It's cheap, but nonzero.

The direction currently taken by the standard library is, somewhat surprisingly, to be fast and unsafe and return the reference. (The only function I know that actually does this is std::get from <tuple>.) As it happens, I've presented a proposal to the C++ core language committee toward the resolution of this issue, a revision is in the works, and just today I've started investigating implementation. But it's complicated, and not a sure thing.

std::string transform(std::string&& input)
{
    return transform(input); // calls the lvalue reference version
}

The compiler won't generate a move here. If input weren't a reference at all, and you did return input; it would, but it has no reason to believe that transform will return input just because it was a parameter, and it won't deduce ownership from rvalue reference type anyway. (See C++14 §12.8/31-32.)

You need to do:

return std::move( transform( input ) );

or equivalently

transform( input );
return std::move( input );
Equilibrium answered 7/5, 2015 at 8:44 Comment(0)
S
1

Some (non-representative) runtimes for the above versions of transform:

run on coliru

#include <iostream>
#include <time.h>
#include <sys/time.h>
#include <unistd.h>

using namespace std;

double GetTicks()
{
    struct timeval tv;
    if(!gettimeofday (&tv, NULL))
        return (tv.tv_sec*1000 + tv.tv_usec/1000);
    else
        return -1;
}

std::string& transform(std::string& input)
{
    // transform the input string
    // e.g toggle first character
    if(!input.empty())
    {
        if(input[0]=='A')
            input[0] = 'B';
        else
            input[0] = 'A';
    }
    return input;
}

std::string&& transformA(std::string&& input)
{
    return std::move(transform(input));
}

std::string transformB(std::string&& input)
{
    return transform(input); // calls the lvalue reference version
}

std::string transformC(std::string&& input)
{
    return std::move( transform( input ) ); // calls the lvalue reference version
}


string getSomeString()
{
    return string("ABC");
}

int main()
{
    const int MAX_LOOPS = 5000000;

    {
        double start = GetTicks();
        for(int i=0; i<MAX_LOOPS; ++i)
            string s = transformA(getSomeString());
        double end = GetTicks();

        cout << "\nRuntime transformA: " << end - start << " ms" << endl;
    }

    {
        double start = GetTicks();
        for(int i=0; i<MAX_LOOPS; ++i)
            string s = transformB(getSomeString());
        double end = GetTicks();

        cout << "\nRuntime transformB: " << end - start << " ms" << endl;
    }

    {
        double start = GetTicks();
        for(int i=0; i<MAX_LOOPS; ++i)
            string s = transformC(getSomeString());
        double end = GetTicks();

        cout << "\nRuntime transformC: " << end - start << " ms" << endl;
    }

    return 0;
}

output

g++ -std=c++14 -O2 -Wall -pedantic -pthread main.cpp && ./a.out

Runtime transformA: 444 ms
Runtime transformB: 796 ms
Runtime transformC: 434 ms
Summertree answered 7/5, 2015 at 9:46 Comment(2)
Does the measure change with bigger strings (that don't fit SSO)?Circumnutate
@Hiura: Just follow the COLIRU link above, press Edit, and change the code as you like and give it a try.Summertree
S
0

This leads me to believe a copy would happen from the std::string& return value of the lvalue reference version of transform(...) into the std::string return value.

Is that correct?

The return reference version will not let std::string copy happened, but the return value version will have copy, if the compiler does not do RVO. However, RVO has its limitation, so C++11 add r-value reference and move constructor / assignment / std::move to help handle this situation. Yes, RVO is more efficient than move semantic, move is cheaper than copy but more expensive than RVO.

Is it better to keep my std::string&& transform(...) version?

This is somehow interesting and strange. As Potatoswatter answered,

std::string transform(std::string&& input)
{
    return transform(input); // calls the lvalue reference version
} 

You should call std::move manually.

However, you can click this developerworks link: RVO V.S. std::move to see more detail, which explain your problem clearly.

Subglacial answered 12/11, 2015 at 14:44 Comment(0)
I
-1

if your question is pure optimization oriented it's best to not worry about how to pass or return an argument. the compiler is smart enough to strech your code into either pure-reference passing , copy elision, function inlining and even move semantics if it's the fastest method.

basically, move semantics can benefit you in some esoteric cases. let's say I have a matrix objects that holds double** as a member variable and this pointer points to a two dimenssional array of double. now let's say I have this expression:
Matrix a = b+c;
a copy constructor (or assigment operator, in this case) will get the sum of b and c as a temorary, pass it as const reference, re-allocate m*namount of doubles on a inner pointer, then, it will run on a+b sum-array and will copy its values one by one. easy computation shows that it can take up to O(nm) steps (which can be generlized to O(n^2)). move semantics will only re-wire that hidden double** out of the temprary into a inner pointer. it takes O(1).
now let's think about std::string for a moment: passing it as a reference takes O(1) steps (take the memory addres, pass it , dereference it etc. , this is not linear in any sort). passing it as r-value-reference requires the program to pass it as a reference, re-wire that hidden underlying C-char* which holds the inner buffer, null the original one (or swap between them), copy size and capacity and many more actions. we can see that although we're still in the O(1) zone - there can be actualy MORE steps than simply pass it as a regular reference.

well, the truth is that I didn't benchmarked it, and the discussion here is purely theoratical. never the less, my first paragraph is still true. we assume many things as developers, but unless we benchmark everything to death - the compiler simply knows better than us in 99% of the time

taking this argument into acount, I'd say to keep it as a reference-pass and not move semantics since it's backword compatible and much more understood for developers who didn't master C++11 yet.

Ickes answered 7/5, 2015 at 8:3 Comment(2)
I agree with you from a practical perspective. However, i think this still is a very good question to help in understanding rValue references. I've been working with rValue refences for quite some time now but this aspect I would still like to understand a bit better:).Riel
The OP asked a question about move semantics for which there are definite answers. The OP is trying to understand something important in the language. Your answer of "...it's best to not worry...", "...move semantics can benefit you in some esoteric cases.". Is not helpful. Move semantics is not an esoteric thing; it's an integral part of the language.Diplocardiac

© 2022 - 2024 — McMap. All rights reserved.