Call to conversion operator instead of converting constructor in c++17 during overload resolution
Asked Answered
C

1

22

Consider the following two classes :

#define PRETTY(x) (std::cout << __PRETTY_FUNCTION__ << " : " << (x) << '\n')

struct D;

struct C {
    C() { PRETTY(this);}
    C(const C&) { PRETTY(this);}
    C(const D&) { PRETTY(this);}
};

struct D {
    D() { PRETTY(this);}
    operator C() { PRETTY(this); return C();}
};

We are interested in the overload resolution between the two constructors :

C::C(const C&);
C::C(const D&);

This code work as expected :

void f(const C& c){ PRETTY(&c);}
void f(const D& d){ PRETTY(&d);}
/*--------*/
D d;
f(d); //calls void f(const D& d)

since void f(const D& d) is a better match.

But :

D d;
C c(d);

(as you can see here)

calls D::operator C() when compiling with std=c++17, and calls C::C(const D&) with std=c++14 on both clang and gcc HEAD. Moreover this behaviour has changed recently : with clang 5, C::C(const D&) is called with both std=c++17 and std=c++14.

What is the correct behaviour here ?

Tentative reading of the standard (latest draft N4687) :

C c(d) is a direct-initialization which is not a copy elision ([dcl.init]/17.6.1). [dcl.init]/17.6.2 tells us that applicable constructors are enumerated and that the best one is chosen by overload resolution. [over.match.ctor] tells us that the applicable constructors are in this case all the constructors.

In this case : C(), C(const C&) and C(const D&) (no move ctor). C() is clearly not viable and thus is discarded from the overload set. ([over.match.viable])

Constructors have no implicit object parameter and so C(const C&) and C(const D&) both take exactly one parameter. ([over.match.funcs]/2)

We now go to [over.match.best]. Here we find that we need to determine which of these two implicit conversion sequences (ICS) is better. The ICS of C(const D&) only involves a standard conversion sequence, but the ICS of C(const C&) involves a user-defined conversion sequence.

Therefore C(const D&) should be selected instead of C(const C&).


Interestingly, these two modifications both causes the "right" constructor to be called :

operator C() { /* */ } into operator C() const { /* */ }

or

C(const D&) { /* */ } into C(D&) { /* */ }

This is what would happen (I think) in the copy-initialization case where user-defined conversions and converting constructors are subject to overload resolution.


As Columbo recommends I filed a bug report with gcc and clang

gcc bug https://gcc.gnu.org/bugzilla/show_bug.cgi?id=82840

clang bug https://bugs.llvm.org/show_bug.cgi?id=35207

Cholecalciferol answered 4/11, 2017 at 12:41 Comment(0)
R
14

Core issue 243 (17 years old!):

There is a moderately serious problem with the definition of overload resolution. Consider this example:

struct B;
struct A {
    A(B);
};
struct B {
    operator A();
} b;
int main() {
    (void)A(b);
} 

This is pretty much the definition of "ambiguous," right? You want to convert a B to an A, and there are two equally good ways of doing that: a constructor of A that takes a B, and a conversion function of B that returns an A.

What we discover when we trace this through the standard, unfortunately, is that the constructor is favored over the conversion function. The definition of direct-initialization (the parenthesized form) of a class considers only constructors of that class. In this case, the constructors are the A(B) constructor and the (implicitly-generated) A(const A&) copy constructor. Here's how they are ranked on the argument match:

  • A(B): exact match (need a B, have a B)
  • A(const A&): user-defined conversion (B::operator A used to convert B to A)

In other words, the conversion function does get considered, but it's operating with, in effect, a handicap of one user defined conversion. To put that a different way, this problem is a problem of weighting, not a problem that certain conversion paths are not considered. […]

Notes from 10/01 meeting:

It turns out that there is existing practice both ways on this issue, so it's not clear that it is "broken". There is some reason to feel that something that looks like a "constructor call" should call a constructor if possible, rather than a conversion function. The CWG decided to leave it alone.

Seems like you're right. Moreover, Clang and GCC selecting the conversion operator is hardly the best choice, neither according to wording nor intuition, so unless this is due to backward compatibility (and even then), a bug report would be appropriate.

Roselleroselyn answered 4/11, 2017 at 14:19 Comment(1)
Comments are not for extended discussion; this conversation has been moved to chat.Willwilla

© 2022 - 2024 — McMap. All rights reserved.