Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

Tags:

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

like image 741
Bruno Avatar asked Nov 04 '17 12:11

Bruno


People also ask

What is a conversion operator in CPP?

Conversion Operators in C++ C++ supports object oriented design. So we can create classes of some real world objects as concrete types. Sometimes we need to convert some concrete type objects to some other type objects or some primitive datatypes. To make this conversion we can use conversion operator.

How do you define implicit conversion in C++?

An implicit conversion sequence is the sequence of conversions required to convert an argument in a function call to the type of the corresponding parameter in a function declaration. The compiler tries to determine an implicit conversion sequence for each argument.

What is a default constructor what is a conversion constructor?

A conversion constructor is a single-parameter constructor that is declared without the function specifier explicit . The compiler uses conversion constructors to convert objects from the type of the first parameter to the type of the conversion constructor's class.

What is an user-defined type conversion?

Conversion functions define conversions from a user-defined type to other types. These functions are sometimes referred to as "cast operators" because they, along with conversion constructors, are called when a value is cast to a different type.


1 Answers

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.

like image 158
Columbo Avatar answered Oct 03 '22 05:10

Columbo