6

For example:

void f(T&& t); // probably making a copy of t

void g() 
{
    T t;
    // do something with t
    f(std::move(t));
    // probably something else not using "t"
}

Is void f(T const& t) equivalent in this case because any good compiler will produce the same code? I'm interested in >= VC10 and >= GCC 4.6 if this matters.

EDIT:

Based on the answers, I'd like to elaborate the question a bit:

Comparing rvalue-reference and pass-by-value approaches, it's so easy to forgot to use std::move in pass-by-value. Can compiler still check that no more changes are made to the variable and eliminate an unnecessary copy?

rvalue-reference approach makes only optimized version "implicit", e.g. f(T()), and requires the user to explicitly specify other cases, like f(std::move(t)) or to explicitly make a copy f(T(t)); if the user isn't done with t instance. So, in this optimization-concerned light, is rvalue-reference approach considered good?

Andriy Tylychko
  • 15,967
  • 6
  • 64
  • 112
  • 1
    There are not many times you should be taking an argument by rvalue reference other than when implementing a move constructor. And almost always it should be overloading a function that takes by `const` lvalue reference. – Joseph Mansfield Mar 25 '13 at 11:59
  • You don't need std::move(T(t), just T(t) will be enough since it is explicit T is no longer used after this function – Alon Mar 25 '13 at 12:34
  • @AndyT You have a misunderstanding about taking an rvalue reference argument. That doesn't perform a move at all. – Joseph Mansfield Mar 25 '13 at 12:35
  • @Alon: yeah, corrected that immediately after posting :) – Andriy Tylychko Mar 25 '13 at 12:38
  • @sftrabbit: please elaborate. I believe I do understand that rvalue-reference doesn't perform a move but requires it in some cases (when a variable has a name) – Andriy Tylychko Mar 25 '13 at 12:42
  • @AndyT Are you talking about when you do `std::move(variable)`? Just to be clear, `std::move` doesn't move anything either. It just makes it moveable. But nothing is moved. – Joseph Mansfield Mar 25 '13 at 13:11
  • Related https://stackoverflow.com/questions/21605579/how-true-is-want-speed-pass-by-value – luca Feb 12 '22 at 13:02
  • And also https://stackoverflow.com/questions/33872026/copy-elision-for-pass-by-value-arguments – luca Feb 12 '22 at 13:05

3 Answers3

5

It's definitely not the same. For once T && can only bind to rvalues, while T const & can bind both to rvalues and to lvalues. Second, T const & does not permit any move optimizations. If you "probably want to make a copy of t", then T && allows you to actually make a move-copy of t, which is potentially more efficient.

Example:

void foo(std::string const & s) { std::string local(s); /* ... */ }

int main()
{
    std::string a("hello");
    foo(a);
}

In this code, the string buffer containing "hello" must exist twice, once in the body of main, and another time in the body of foo. By contrast, if you used rvalue references and std::move(a), the very same string buffer can be "moved around" and only needs to be allocated and populated one single time.

As @Alon points out, the right idiom is in fact passing-by-value:

void foo(std::string local) { /* same as above */ }

int main()
{
    std::string a("hello");
    foo(std::move(a));
}
Kerrek SB
  • 464,522
  • 92
  • 875
  • 1,084
  • 1
    @KerrekSB, Yeah, please do check the edited question... Also, your "for one" sentence sounds 'lawyery' and evasive; the rest sounds like the real answer. – einpoklum Aug 07 '17 at 19:00
2

Well, it depends what f does with t, if it creates a copy of it, then I would even go at length of doing this:

void f(T t) // probably making a copy of t
{
    m_newT = std::move(t); // save it to a member or take the resources if it is a c'tor..
}

void g() 
{
    T t;
    // do something with t
    f(std::move(t));
    // probably something else not using "t"
}

Then you allow the move c'tors optimization to happen, you take 't' resources in any case, and if it was 'moved' to your function, then you even gain the non copy of moving it to the function, and if it was not moved then you probably had to have one copy

Now if at later on in the code you'd have:

f(T());

Then ta da, free move optimization without the f user even knowing..

Note that quote: "is void f(T const& t) equivalent in this case because any good compiler will produce the same code?"

It is not equivelent, it is LESS work, because only the "pointer" is transferred and no c'tors are called at all, neither move nor anything else

Alon
  • 1,776
  • 13
  • 31
  • Why would you make a local copy, and *then* move it into another local copy? – juanchopanza Mar 25 '13 at 11:48
  • @juanchopanza: You don't actually make a local copy, since move c'tor is called, when you HAVE to make a local copy(and then take its resources so it's one copy either way) and you can't use move – Alon Mar 25 '13 at 11:49
  • @juanchopanza: what if `f` is a constructor? – Andriy Tylychko Mar 25 '13 at 11:50
  • You make a copy because you are taking the argument by value. – juanchopanza Mar 25 '13 at 11:51
  • @juanchopanza (That is the all essence of move c'tors :) ), the 'copy' you make does not allocate any new resources – Alon Mar 25 '13 at 11:52
  • You already have `t`. You have no need to move into `newT`. You can just use `t`. – juanchopanza Mar 25 '13 at 11:55
  • @Alon The point is there's no reason to do that. `t` is a local variable, whether it was copied or moved from, it's still local to that function and you don't need to copy/move it into `newT`. – Joseph Mansfield Mar 25 '13 at 12:01
  • @sftrabbit: can't believe you guys got stuck on that.. I gave it as an example on how to save it to a member.. – Alon Mar 25 '13 at 12:04
1

Taking an const lvalue reference and taking an rvalue reference are two different things.

Similarities:

  • Neither will cause an copy or move to take place because they are both references. A reference just references an object, it doesn't copy/move it in any way.

Differences:

  • A const lvalue reference will bind to anything (lvalue or rvalue). An rvalue reference will only bind to non-const rvalues - much more limited.

  • The parameter inside the function cannot be modified when it is a const lvalue reference. It can be modified when it's an rvalue reference (since it is non-const).

Let's look at some examples:

  1. Taking const lvalue reference: void f(const T& t);

    1. Passing an lvalue:

      T t; f(t);
      

      Here, t is an lvalue expression because it's the name of the object. A const lvalue reference can bind to anything, so t will happily be passed by reference. Nothing is copied, nothing is moved.

    2. Passing an rvalue:

      f(T());
      

      Here, T() is an rvalue expression because it creates a temporary object. Again, a const lvalue reference can bind to anything, so this is okay. Nothing is copied, nothing is moved.

    In both of these cases, the t inside the function is a reference to the object passed in. It can't be modified by the reference is const.

  2. Taking an rvalue reference: `void f(T&& t);

    1. Passing an lvalue:

      T t;
      f(t);
      

      This will give you a compiler error. An rvalue reference will not bind to an lvalue.

    2. Passing an rvalue:

      f(T());
      

      This will be fine because an rvalue reference can bind to an rvalue. The reference t inside the function will refer to the temporary object created by T().

Now let's consider std::move. First things first: std::move doesn't actually move anything. The idea is that you give it an lvalue and it turns it into an rvalue. That's all it does. So now, if your f takes an rvalue reference, you could do:

T t;
f(std::move(t));

This works because, although t is an lvalue, std::move(t) is an rvalue. Now the rvalue reference can bind to it.

So why would you ever take an rvalue reference argument? In fact, you shouldn't need to do it very often, except for defining move constructors and assignment operators. Whenever you define a function that takes an rvalue reference, you almost certainly want to give a const lvalue reference overload. They should almost always come in pairs:

void f(const T&);
void f(T&&);

Why is this pair of functions useful? Well, the first will be called whenever you give it an lvalue (or a const rvalue) and the second will be called whenever you give it a modifiable rvalue. Receiving an rvalue usually means that you've been given a temporary object, which is great news because that means you can ravage its insides and perform optimizations based on the fact that you know it's not going to exist for much longer.

So having this pair of functions allows you to make an optimization when you know you're getting a temporary object.

There's a very common example of this pair of functions: the copy and move constructors. They are usually defined like so:

T::T(const T&); // Copy constructor
T::T(T&&); // Move constructor

So a move constructor is really just a copy constructor that is optimized for when receiving a temporary object.

Of course, the object being passed isn't always a temporary object. As we've shown above, you can use std::move to turn an lvalue into an rvalue. Then it appears to be a temporary object to the function. Using std::move basically says "I allow you to treat this object as a temporary object." Whether it actually gets moved from or not is irrelevant.

However, beyond writing copy constructors and move constructors, you'd better have a good reason for using this pair of functions. If you're writing a function that takes an object and will behave exactly the same with it regardless of whether its a temporary object or not, simply take that object by value! Consider:

void f(T t);

T t;
f(t);
f(T());

In the first call to f, we are passing an lvalue. That will be copied into the function. In the second call to f, we are passing an rvalue. That object will be moved into the function. See - we didn't even need to use rvalue references to cause the object to be moved efficiently. We just took it by value! Why? Because the constructor that is used to make the copy/move is chosen based on whether the expression is an lvalue or an rvalue. Just let the copy/move constructors do their job.

As to whether different argument types result in the same code - well that's a different question entirely. The compiler operates under the as-if rule. This simply means that as long as the program behaves as the standard dictates, the compiler can emit whatever code it likes. So the functions may emit the same code if they happen to do exactly the same thing. Or they may not. However, it's a bad sign if you're functions that take a const lvalue reference and an rvalue reference are doing the same thing.

Joseph Mansfield
  • 108,238
  • 20
  • 242
  • 324