10

Why should I or shouldn't I create all my functions and members functions to take a rvalue and leave out versions that take a lvalue? You can always forward lvalue to rvalue, right? I can even have const rvalue, so why is this a bad idea, or a good idea?

What I mean in code is below. The "&&" rvalue reference allows the users to use temporaries and can still use lvalue by a simple forward. So with this in mind, why should I provide print_string(string& str) (lvalue reference) of any function in c++11 (besides const reference, because rvalue there is fine)?

#include <iostream>
#include <string>
#include <utility>

using namespace std;


void print_string(string&& str)
{
    str += "!!!";
    cout << str << endl;
}
void print_string2(string& str)
{
    str += "???";
    cout << str << endl;
}

int main(int argc, char* argv[])
{

    print_string("hi there");  // works
    print_string(string("hi again")); // works
    string lvalue("lvalue");
    print_string(forward<string>(lvalue)); // works as expected
    //print_string(lvalue);  // compile error

    //print_string2("hi there"); // comile error
    //print_string2(string("hi again")); // compile error
    print_string2(lvalue);


    char a;
    cin >> a;
}

output

hi there!!!
hi again!!!
lvalue!!!
lvalue!!!???

void print_string(string&& str) provides a more flexible use case than void print_string2(string& str) so why shouldn't we use rvalue arguments all the time?

stewart99
  • 14,024
  • 7
  • 27
  • 42
  • 1
    No! Take an rvalue where it makes sense (apply `std::move` or provide a reasonable `std::swap` implementation), otherwise stick to `const &` or value passed parameters as already usual. You might notice, that the rule-of-three might become the rule-of-five with consideration of the new c++11 features. – πάντα ῥεῖ Dec 26 '13 at 19:10
  • 2
    Doing something as a rigid rule, without applying enough thought is dangerous, at best. Use the right tool for the right job. By using an r-value reference it's clear that the resource managed is movable, while using const l-value ref tells otherwise. – legends2k Dec 26 '13 at 19:13
  • 8
    Rigid rule: don't stop thinking. – R. Martinho Fernandes Dec 26 '13 at 20:39
  • 1
    That would explode the usage of `std::foward`/`std::move` for myriad call-sites, which is stupid. – oblitum Dec 28 '13 at 19:47
  • I still don't understand why, of all the new features, r-value references is such a lightning rod. It is a minor feature at best that was purpose built to solve two specific problems. 99% of the new features of C++11 are more useful to know about and use. – Tim Seguine Dec 29 '13 at 00:11

3 Answers3

7

As with many features of C++, there is a convention of how to use lvalue and rvalue references.

Another example for conventions are the operators, especially == and !=. You could overload them to return doubles, and copy files for the comparison. But per convention, they return bool and only compare the left and right operand without modifications.


The convention for rvalue references is that they refer to objects whose resources can be "stolen":

  • an lvalue reference implies that the object is owned by someone else
  • an rvalue reference implies that the we can steal the resources from the object

Rvalue references have been introduced to support move semantics, that is, transfer of ownership of resources. Move semantics conventionally implies that an object bound to an rvalue reference can be moved from. Moved-from objects conventionally are assumed to be in a valid, but unknown state. For example:

// returns a string that equals i appended to p
string join(string&& p, int i); // string&& means: I do something to p. Or not.

// append i to p
void append(string& p, int i);


string my_string = "hello, world."; // length is 13

append(my_string, 42); // I know what state my_string is specified to be in now
my_string[12] = "!"; // guaranteed to work

string result = join( std::move(my_string), 42 );
// what state is my_string now in?
char c = my_string[1]; // is this still guaranteed to work?


string const cresult = join("hello, number ", 5);
// fine, I don't care what state the temporary is in -- it's already destroyed

One could think of definitions like:

string join(string&& p, int i)
{  return std::move(p) + std::to_string(i);  }

void append(string& p, int i)
{  p += std::to_string(i);  }

This definition of join really does steal the resource from p, and the Standard really doesn't specify the state of p after the operation std::move(p) + std::to_int(i) (which creates a temporary that steals the resource from p).

For this simple example, you could still use

string result = "hello ";
result = join(std::move(result), 42);

to "replace" append. But moves can also be expensive (they always also copy something), and multiple lvalue reference parameters cannot easily be replaced by this technique.

N.B. in my opinion, you should indicate in the name of a function when it takes an lvalue reference and modifies the argument. I certainly don't expect a function named print_string2 to modify the string I pass in.


IMHO, it's the convention that the moved-from object is in a valid, but unspecified state that should keep you from using rvalue references everywhere.

Additionally, there can be a slight performance impact when using too many moves as opposed to in-place manipulation à la join.

dyp
  • 38,334
  • 13
  • 112
  • 177
5

Various stackexchange questions have helped me answer my own question.

I had forward and move confused. This answer helped (Can I typically/always use std::forward instead of std::move?) You can r/forward/move/g and my question is still the same. Be sure to read Scott Meyer's response to this question directly: http://scottmeyers.blogspot.co.uk/2012/11/on-superfluousness-of-stdmove.html

So is it a good idea? Well, these posts helped me figure that out: Is pass-by-value a reasonable default in C++11?

And

Should all/most setter functions in C++11 be written as function templates accepting universal references?

And

Should I always move on `sink` constructor or setter arguments?

Community
  • 1
  • 1
stewart99
  • 14,024
  • 7
  • 27
  • 42
  • 3
    Without linking a bunch of stuff that folks have to read through, can you summarize what you learnt in the answer itself? – dev_nut Nov 04 '18 at 21:35
1

print_string2 is a bad design. It modifies the string. For example someone might write:

string s{"hello"};
print_string2(s);     // print the string

foo(s);      // work on the string - oops, it has junk punctuation on it now.

If a function modifies the caller's string then it should not be called print_something.

So you're right that string&& is better than string&, but both of those options are sub-optimal.

Instead, this function could just accept a string by value:

void print_string2(string s)

Then if you call it with an rvalue (including xvalues) the argument is moved into s, and if you call it with an lvalue then a copy is made, which is the ideal behaviour.

If your function does not need to modify the string as part of its processing then you should pass by const reference, which can bind to both rvalues and lvalues:

void print_string(const string &s) 
{

In your examples this is the best option, as then there is no need to duplicate a string when printing lvalues. The body would be:

cout << str << "???" << endl;
M.M
  • 138,810
  • 21
  • 208
  • 365