13

Given the following code snippet:

struct Foo {
};

struct Bar {
    operator const Foo() {
        return Foo();
    }
};

int main() {
    Bar bar;
    Foo foo(bar);
    return 0;
}

See here on godbolt


It compiles fine with gcc 11.2 but fails to compile with clang 12.0 with the following error:

<source>:12:13: error: no viable conversion from 'Bar' to 'Foo'
    Foo foo(bar);
            ^~~
<source>:1:8: note: candidate constructor (the implicit copy constructor) not viable: no known conversion from 'Bar' to 'const Foo &' for 1st argument
struct Foo {
       ^
<source>:1:8: note: candidate constructor (the implicit move constructor) not viable: no known conversion from 'Bar' to 'Foo &&' for 1st argument
struct Foo {
       ^
<source>:5:5: note: candidate function
    operator const Foo() {
    ^
<source>:1:8: note: passing argument to parameter here
struct Foo {
       ^

  • Which implementation is correct?
  • Is this actually valid C++?

PS: I know it can be fixed by removal of the const or return of a const reference.

8
  • Out of GCC, Clang and MSVC only Clang produces this error, only for direct-initialization (not copy-initialization or reference initialization) and only in C++14 mode or lower. Commented Jan 10, 2022 at 14:14
  • Moreover, it compiles fine provided copy constructor is explicitly declared for foo: godbolt.org/z/fz8KE11ns Commented Jan 10, 2022 at 14:21
  • 1
    Changing the C++ standard to 17 and above on clang 12 allows this to work. gcc.godbolt.org/z/KhGxzo6xs Commented Jan 10, 2022 at 14:22
  • 2
    @alagner Declaring the copy constructor inhibits declaration of the implicit move constructor, which is what Clang seems to try to use in the construction. Maybe this is CWG 2077. Commented Jan 10, 2022 at 14:26
  • This is interesting: godbolt.org/z/YM15T5Gc3 it fails on clang only when C++11 or C++14 is selected. It is fine even for C++03. Commented Jan 10, 2022 at 14:32

1 Answer 1

8

I think this is the open CWG issue 2077.

Basically,

Foo foo(bar);

is direct-initialization, meaning that it will consider the constructors of Foo for overload resolution and choose the best viable one. The candidates are the implicit copy and move constructor with signatures

Foo(const Foo&);
Foo(Foo&&);

If you look at the linked CWG issue and for more details the related Clang bug, then we see that this is exactly the situation described there, only that our function call is a constructor call, while theirs is a normal function call.

If we simply believe the argument made in the CWG issue, then overload resolution should apparently by current rules pick the Foo&& overload, while not actually being allowed to bind the reference, resulting in an ill-formed program.


I will try to reproduce the reasoning from the linked sources here:

When considering conversions to the constructor's parameter from bar, the reference cannot directly bind bar because of the type mismatch.

According to [over.ics.ref]/2 in such a case the conversion sequence is determined as if by copy-initialization of a temporary of the referenced type.

In this case the conversion is Bar -> const Foo -> Foo via the user-defined constructor for the move constructor. For the copy constructor it is Bar -> const Foo.

However in the copy-initialization of the temporary, const-qualifier conversions are "subsumed" in the initialization. [over.ics.ref]/2 Therefore the move constructor sequence is not worse than that of the copy constructor.

In such a case the rvalue reference is a tie-breaker and so the move constructor is chosen in overload resolution.

However, a const Foo cannot be bound to a non-const rvalue reference. Therefore the program is ill-formed. ([dcl.init.ref]/5.4.3, DR 1604)

Also note that the rest of [over.ics.ref] gives requirements on the binding of references to influence the viability of an overload, but do not include binding of const rvalues to non-const rvalue references.


This is not a problem in e.g. copy-initialization (Foo foo = bar;), because that can use the conversion operator directly without going through the constructor.

It is also not a problem before C++11, because the move constructor doesn't exist there.

For the same reason it is not a problem if the copy constructor of Foo is explicitly declared, because that would inhibit the declaration of the implicit move constructor.

It is also not a problem in C++17 and later because mandatory copy elision makes it so the move constructor will never be called.

I think this explains all of Clang's behaviors.


GCC and MSVC seem to choose the copy constructor instead of the move constructor in overload resolution, which while apparently not correct by the current wording of the standard, is probably what is intended to happen and what the linked CWG issue aims for, by removing the function overload with the ill-formed reference binding from the set of viable functions.

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.