views:

490

answers:

5

(I asked a variation of this question on comp.std.c++ but didn't get an answer.)

Why does the call to f(arg) in this code call the const ref overload of f?

void f(const std::string &); //less efficient
void f(std::string &&); //more efficient

void g(const char * arg)
{
     f(arg);
}

My intuition says that the f(string &&) overload should be chosen, because arg needs to be converted to a temporary no matter what, and the temporary matches the rvalue reference better than the lvalue reference.

This is not what happens in GCC and MSVC (edit: Thanks Sumant: it doesn't happen in GCC 4.3-4.5). In at least G++ and MSVC, any lvalue does not bind to an rvalue reference argument, even if there is an intermediate temporary created. Indeed, if the const ref overload isn't present, the compilers diagnose an error. However, writing f(arg + 0) or f(std::string(arg)) does choose the rvalue reference overload as you would expect.

From my reading of the C++0x standard, it seems like the implicit conversion of a const char * to a string should be considered when considering if f(string &&) is viable, just as when passing a const lvalue ref arguments. Section 13.3 (overload resolution) doesn't differentiate between rvalue refs and const references in too many places. Also, it seems that the rule that prevents lvalues from binding to rvalue references (13.3.3.1.4/3) shouldn't apply if there's an intermediate temporary - after all, it's perfectly safe to move from the temporary.

Is this:

  1. Me misreading/misunderstand the standard, where the implemented behavior is the intended behavior, and there's some good reason why my example should behave the way it does?
  2. A mistake that the compiler vendors have somehow all made? Or a mistake based on common implementation strategies? Or a mistake in e.g. GCC (where this lvalue/rvalue reference binding rule was first implemented), that was copied by other vendors?
  3. A defect in the standard, or an unintended consequence, or something that should be clarified?

EDIT: I have a follow-on question that is related: http://stackoverflow.com/questions/2749263/c0x-rvalue-references-lvalues-rvalue-binding

+2  A: 

A lot of things in the current draft of the standard need clarification, if you ask me. And the compilers are still developing, so it's hard to trust their help.

It looks pretty clear that your intuition is right… temporaries of any kind are supposed to bind to rvalue references. For example, §3.10, the new "taxonomy" section, categorically defines temporaries as rvalues.

The problem may be that the RR argument specification is insufficient to invoke the creation of a temporary. §5.2.2/5: "Where a parameter is of const reference type a temporary object is introduced if needed." That sounds suspiciously exclusive.

Seems to slip through the cracks again at §13.3.3.1/6: (emphasis mine)

When the parameter type is not a reference, the implicit conversion sequence models a copy-initialization of the parameter from the argument expression. The implicit conversion sequence is the one required to convert the argument expression to a prvalue of the type of the parameter.

Note that copy-initialization string &&rr = "hello"; works fine in GCC.

EDIT: Actually the problem doesn't exist on my version of GCC. I'm still trying to figure out how the second standard conversion of the user-defined conversion sequence relates to forming an rvalue reference. (Is RR formation a conversion at all? Or is it dictated by scattered tidbits like 5.2.2/5?)

Potatoswatter
Thanks - I missed those clauses you quoted. They do appear to confuse things.Also, the reason why I didn't pass a string literal is because MSVC appears to treat them as lvalues, but GCC doesn't - it's rather strange.
Doug
A: 

Take a look at this:

http://blogs.msdn.com/vcblog/archive/2009/02/03/rvalue-references-c-0x-features-in-vc10-part-2.aspx

rvalue references: overload resolution

It looks like your case is: "Lvalues strongly prefer binding to lvalue references".

Alex Farber
Yes, but there's an implicit temporary conversion along the way, which makes it a bit different from those cases.
Doug
+2  A: 

I did not see the behavior mentioned by Doug on g++. g++ 4.5 and 4.4.3 both call f(string &&) as expected but VS2010 calls f(const string &). Which g++ version are you using?

Sumant
+1, I wish I'd verified the problem before jumping in!
Potatoswatter
I re-checked in G++ and you're right - it doesn't happen. I'm sure I checked it there before! It seems that maybe the problem is only in MSVC, or it's unclear in the standard.
Doug
A: 

I don't know if that has changed in the latest versions of the standard, but it used to say something like "if in doubt, don't use the rvalue reference". Probably for compatibility reasons.

If you want the move semantics, use f(std::move(arg)), that works with both compilers.

+4  A: 

GCC is doing it wrong according the FCD. The FCD says at 8.5.3 about reference binding

  • If the reference is an lvalue reference and the initializer expression is an [lvalue / class type] ...
  • Otherwise, the reference shall be an lvalue reference to a non-volatile const type (i.e., cv1 shall be const), or the reference shall be an rvalue reference and the initializer expression shall be an rvalue or have a function type.

Your case for the call to the std::string && matches none of them, because the initializer is an lvalue. It doesn't get to the place to create a temporary rvalue, because that toplevel bullet already requires an rvalue.

Now, overload resolution doesn't directly use reference binding to see whether there exist an implicit conversion sequence. Instead, it says at 13.3.3.1.4/2

When a parameter of reference type is not bound directly to an argument expression, the conversion sequence is the one required to convert the argument expression to the underlying type of the reference according to 13.3.3.1.

Thus, overload resolution figures out a winner, even though that winner may actually not be able to bind to that argument. For example:

struct B { B(int) { /* ... */ } };
struct A { int bits: 1; };

void f(int&);
void f(B);
int main() { A a; f(a.bits); }

Reference binding at 8.5 forbids bitfields to bind to lvalue references. But overload resolution says that the conversion sequence is the one converting to int, thus succeeding even though when the call is made later, the call is ill-formed. Thus my bitfields example is ill-formed. If it was to choose the B version, it would have succeeded, but needed a user defined conversion.

However, there exist two exceptions for that rule. These are

Except for an implicit object parameter, for which see 13.3.1, a standard conversion sequence cannot be formed if it requires binding an lvalue reference to non-const to an rvalue or binding an rvalue reference to an lvalue.

Thus, the following call is valid:

struct B { B(int) { /* ... */ } };
struct A { int bits: 1; };

void f(int&); /* binding an lvalue ref to non-const to rvalue! */
void f(B);
int main() { A a; f(1); }

And thus, your example calls the const T& version

void f(const std::string &);
void f(std::string &&); // would bind to lvalue!

void g(const char * arg) { f(arg); }

However, if you say f(arg + 0), you create an rvalue, and thus the second function is viable.

Johannes Schaub - litb
Ah, I didn't read 8.5.3 too closely, thanks. It does seem like the rules for binding references are unnecessarily strict - I don't see that there's any compelling use case to reject such code, but it can (unexpectedly, IMO) cause unintended copies - e.g. if you called vector<string>::push_back with a const char * lvalue argument.
Doug
One more thing - the prohibition on non-const lvalue ref args accepting temporaries (to me) is specifically so you don't accidentally lose any "extra" information a function might be returning through that reference. But this consideration doesn't apply to an rvalue ref, whether const or non-const - it is conceptually "not a useful value" after the call. Finally, the fact that f(arg) is disallowed but f(arg + 0) is allowed, or that e.g. stringvec.push_back(string(charptr)) is *more efficient* than stringvec.push_back(charptr) is very unintuitive to me.
Doug