5

The following code compiles fine with g++ (GCC) 4.7.1 20120721, but fails with a recently build clang version 3.2 (trunk).

struct Y {};

struct X {
  operator const Y() const { return Y(); }
};

void f(Y&& y) {}

int main()
{
  f(X());
  return 0;
}

Changing the conversion operator to operator Y() const is sufficient to make the code compile on both compilers.

Which compiler is actually standard compliant in this case? What does the standard actually say about this?

The verbatim error as requested:

bla.cpp:14:5: error: no viable conversion from 'X' to 'Y'
  f(X());
    ^~~
bla.cpp:1:8: note: candidate constructor (the implicit copy constructor) not viable: no known conversion from 'X' to
      'const Y &' for 1st argument
struct Y {
       ^
bla.cpp:1:8: note: candidate constructor (the implicit move constructor) not viable: no known conversion from 'X' to
      'Y &&' for 1st argument
struct Y {
       ^
bla.cpp:6:3: note: candidate function
  operator const Y() const { return Y(); }
  ^
bla.cpp:10:12: note: passing argument to parameter 'y' here
void f(Y&& y) {}
       ^

EDIT: Unfortunately even adding the overload

void f(const Y&) {}

still makes clang choose the rvalue reference overload and so this breaks existing code that used to compile fine, e.g. with standard containers.

pmr
  • 58,701
  • 10
  • 113
  • 156
  • Does it really say "`R&&`"? Can you post the diagnostic verbatim? – Lightness Races in Orbit Oct 08 '12 at 10:36
  • 2
    For the record, as a total guess, I'd say clang is right here. – Lightness Races in Orbit Oct 08 '12 at 10:36
  • @LightnessRacesinOrbit I dumped the diagnostic. The problem is that it doesn't state `why` the conversion operator is not applicable. – pmr Oct 08 '12 at 10:41
  • @LightnessRacesinOrbit Because it shouldn't convert lvalue (const Y) to rvalue (Y&&)? – BЈовић Oct 08 '12 at 10:41
  • @BЈовић: Yeah, but my rvalue-ref fu is not sufficient to make any more than a wild intuitive guess – Lightness Races in Orbit Oct 08 '12 at 10:45
  • @BЈовић But that leaves me with the question how to write that kind of code: returning `const Y()` was useful in C++03 to prevent calling of non-const members on the return value and AFAIK I cannot have an `operator Y&&`. – pmr Oct 08 '12 at 10:48
  • 1
    On one hand, you say you don't want to use non-const members on that object (returning `const Y`); on the other hand, you say you do (asking for a `Y&&`). There is something inconsistent here. – Gorpik Oct 08 '12 at 10:56
  • 1
    It seems fairly clear. A temporary of type `Y const` cannot bind to a reference of type `Y &&`. It *could* bind to a `Y const &&`, but that'd be fairly useless. – Kerrek SB Oct 08 '12 at 11:17
  • @pmr "returning `const Y()` was useful in C++03 to prevent calling of non-const members on the return value" -- and now, with rvalue references, calling non-const members on the return value makes sense, so you should no longer prevent it. –  Oct 08 '12 at 11:23
  • @hvd I perfectly agree with you. I still expect my legacy code to work without touching it. – pmr Oct 08 '12 at 17:08
  • @pmr About your edit, the behaviour you see in your original question makes perfect sense, the behaviour you see in your update doesn't (to me), and it doesn't work like that on my system. `struct S {}; const S f(); char g(S &&); short g(const S &); int main() { return sizeof(g(f())) != sizeof(short); }` returns 0 for me, indicating the `g(const S &)` overload is picked. Does it return 1 for you? –  Oct 10 '12 at 08:43
  • @hvd No it fails to compile. That is how I was made aware of it. I can reproduce this on Apple clang 4.1 and clang 3.2 trunk. This has been reported to the clang bugtracker. – pmr Oct 10 '12 at 09:53
  • @pmr Ah, right, `f()` must of course be a valid argument for `g()` in order to get `g(f())` to compile, even when it's only part of `sizeof`. I cannot say for sure whether the code is valid C++, but if it's invalid, I agree with you that this is a surprising and seemingly unnecessary source of incompatibilities. –  Oct 10 '12 at 10:19

2 Answers2

6

I believe clang is right to reject this. Passing the argument to f(Y&&) requires two conversion steps, the first being your operator const Y() and the second being Y's copy constructor. Both count as user-defined conversions, I think, and both are implicit, which violates the principle that an implicit conversion sequence only includes one user-defined conversion.

This Purpose of returning by const value? contains some interesting insights into the semantics of returning a const T.

Hm, if I try adding an overload void f(const Y&y) as the edited question now does, clang behaves pretty strangly. It still complains about being unable to convert X to Y, and doesn't even list the overload f(const Y& y) in its diagnostic. But once I change the overload to take Y by value, i.e. write void f(const Y y), it complains about the call to f being ambiguous.

This is with XCode 4.5's clang which reports Apple clang version 4.1 (tags/Apple/clang-421.11.66) (based on LLVM 3.1svn). If you can reproduce this with a vanilla clang, you should probably report this on the clang mailing list - sure seems to there's a bug lurking there somewhere...

Community
  • 1
  • 1
fgp
  • 8,126
  • 1
  • 17
  • 18
  • I think the part about the two conversions is spot on. – pmr Oct 08 '12 at 11:43
  • I have amended the question, which makes the whole problem even more difficult. – pmr Oct 08 '12 at 11:58
  • 1
    I also cannot imagine that this should be right. This would trigger a regression in pretty much every similar code that uses STL containers. – pmr Oct 08 '12 at 12:14
2

The example is ill-formed.

Some N3242 quotes.

8.5.3 paragraph 4:

Given types “cv1 T1” and “cv2 T2,” “cv1 T1” is reference-related to “cv2 T2” if T1 is the same type as T2, or T1 is a base class of T2. “cv1 T1” is reference-compatible with “cv2 T2” if T1 is reference-related to T2 and cv1 is the same cv-qualification as, or greater cv-qualification than, cv2.

paragraph 5 (bullet labels are mine):

A reference to type "cv1 T1" is initialized by an expression of type "cv2 T2" as follows:

  1. If the reference is an lvalue reference and ...
  2. 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.

    a. If the initializer expression

    • i. is an xvalue, class prvalue, array prvalue or function lvalue and "cv1 T1" is reference-compatible with "cv2 T2", or
    • ii. has a class type (i.e. T2 is a class type), where T1 is not reference-related to T2, and can be implicitly converted to an xvalue, class prvalue, or function lvalue of type "cv3 T3", where "cv1 T1" is reference-compatible with "cv3 T3",
    • then the reference is bound to ....

    b. Otherwise, a temporary of type "cv1 T1" is created and initialized from the initializer expression using the rules for a non-reference copy-initialization (8.5). The reference is then bound to the temporary. If T1 is reference-related to T2, cv1 shall be the same cv-qualification as, or greater cv-qualification than, cv2. ...

For the function parameter initialization, T1 is Y, T2 is X, and cv1 and cv2 are both empty. 1 is out: the reference is an rvalue reference, not lvalue reference. 2.a.i. is out: X is not reference-compatible with Y. 2.b. is out because copy-initializing an Y from a prvalue of type X involves two user-defined conversions: the conversion function and then the copy constructor of Y. (The exact prohibition is in 13.3.3.1p4.)

For case 2.a.ii., the obvious choice for "cv3 T3" is const Y, but that's no good because Y is not reference-compatible with const Y. You might argue for trying T3 is Y and cv3 is empty, but then you're back to needing the copy constructor of Y as the second implicit user-defined conversion.

aschepler
  • 70,891
  • 9
  • 107
  • 161
  • Are you also considering the case with two overloads? It is not really related as to why the example is ill-formed, but it would solve the real issue. – pmr Oct 08 '12 at 13:42