Conversion constructor vs. conversion operator: precedence
Asked Answered
O

2

82

Reading some questions here on SO about conversion operators and constructors got me thinking about the interaction between them, namely when there is an 'ambiguous' call. Consider the following code:

class A;

class B { 
      public: 
         B(){} 
         
         B(const A&) //conversion constructor
         { 
              cout << "called B's conversion constructor" << endl; 
         } 
};

class A { 
      public: 
         operator B() //conversion operator
         { 
              cout << "called A's conversion operator" << endl; 
              return B(); 
         } 
};

int main()
{
    B b = A(); //what should be called here? apparently, A::operator B()
    return 0;
}

The above code displays "called A's conversion operator", meaning that the conversion operator is called as opposed to the constructor. If you remove/comment out the operator B() code from A, the compiler will happily switch over to using the constructor instead (with no other changes to the code).

My questions are:

  1. Since the compiler doesn't consider B b = A(); to be an ambiguous call, there must be some type of precedence at work here. Where exactly is this precedence established? (a reference/quote from the C++ standard would be appreciated)
  2. From an object-oriented philosophical standpoint, is this the way the code should behave? Who knows more about how an A object should become a B object, A or B? According to C++, the answer is A -- is there anything in object-oriented practice that suggests this should be the case? To me personally, it would make sense either way, so I'm interested to know how the choice was made.
Outcross answered 5/9, 2009 at 18:47 Comment(2)
The line you commented "// copy constructor" is not a copy constructor, it is a constructor.Guinness
You're right, I misused the term. I've edited it out.Outcross
D
63

You do copy initialization, and the candidate functions that are considered to do the conversions in the conversion sequence are conversion functions and converting constructors. These are in your case

B(const A&)
operator B() 

Now, that are the way you declare them. Overload resolution abstracts away from that, and transforms each candidate into a list of parameters that correspond to the arguments of the call. The parameters are

B(const A&)
B(A&)

The second one is because the conversion function is a member function. The A& is the so-called implicit object parameter that's generated when a candidate is a member function. Now, the argument has type A. When binding the implicit object parameter, a non-const reference can bind to an rvalue. So, another rule says that when you have two viable functions whose parameters are references, then the candidate having the fewest const qualification will win. That's why your conversion function wins. Try making operator B a const member function. You will notice an ambiguity.

From an object-oriented philosophical standpoint, is this the way the code should behave? Who knows more about how an A object should become a B object, A or B? According to C++, the answer is A -- is there anything in object-oriented practice that suggests this should be the case? To me personally, it would make sense either way, so I'm interested to know how the choice was made.

For the record, if you make the conversion function a const member function, then GCC will chose the constructor (so GCC seems to think that B has more business with it?). Switch to pedantic mode (-pedantic) to make it cause a diagnostic.


Standardese, 8.5/14

Otherwise (i.e., for the remaining copy-initialization cases), user-defined conversion sequences that can convert from the source type to the destination type or (when a conversion function is used) to a derived class thereof are enumerated as described in 13.3.1.4, and the best one is chosen through overload resolution (13.3).

And 13.3.1.4

Overload resolution is used to select the user-defined conversion to be invoked. Assuming that "cv1 T" is the type of the object being initialized, with T a class type, the candidate functions are selected as follows:

  • The converting constructors (12.3.1) of T are candidate functions.
  • When the type of the initializer expression is a class type "cv S", the conversion functions of S and its base classes are considered. Those that are not hidden within S and yield a type whose cv-unqualified version is the same type as T or is a derived class thereof are candidate functions. Conversion functions that return "reference to X" return lvalues of type X and are therefore considered to yield X for this process of selecting candidate functions.

In both cases, the argument list has one argument, which is the initializer expression. [Note: this argument will be compared against the first parameter of the constructors and against the implicit object parameter of the conversion functions. ]

And 13.3.3.2/3

  • Standard conversion sequence S1 is a better conversion sequence than standard conversion sequence S2 if [...] S1 and S2 are reference bindings (8.5.3), and the types to which the references refer are the same type except for top-level cv-qualifiers, and the type to which the reference initialized by S2 refers is more cv-qualified than the type to which the reference initialized by S1 refers.
Drais answered 5/9, 2009 at 19:5 Comment(6)
Ah, so my problem was not about precedence between the constructor and operator, but the const-ness of each. You were right, changing operator B() to operator B() const resulted in an ambiguity error.Outcross
What a coincidence that i put the same "analysis" on another answer of mine as an example earlier: #1051879 xDDrais
Quick question about the very first sentence of the answer - you do copy initialization.... I don't see a copy occurring anywhere - i.e., there's no A being constructed from another A (nor a B from another B) - so am I correct that you didn't literally mean copy initialization, but rather something along the lines of assignment initialization? Thanks!Harper
Another question regarding the transformation of the conversion constructor B(const A&) as a result of the overload resolution abstraction that you mention near the start of the answer: It is my understanding that all constructors also utilize a this pointer implicitly, just as the operator B() function does (as you've highlighted). Therefore, wouldn't it be correct to write, for the transformed conversion constructor near the top, void(B&,const A&), rather than B(const A&)? Note that one of your own answers seems to indicate as much - see https://mcmap.net/q/77264/-when-is-quot-this-quot-pointer-initialized-in-c.Harper
+1 for mentioned -pedantic. I can't reproduce the ambiguity using example code from cppreference.com without that. Blame g++ for that.Humo
I just tested this on Visual Studio 2022, and it's giving me the opposite result. The converting constructor is called instead of the conversion operator. Any idea why? g++ prefers the operator over the constructorMeatiness
D
3

It seems MSVS2008 has its own opinion about constructor selection: it calls copy constructor in B regardless of constness of A's operator. So be careful here even while standard specifies the correct behavior.

I thought MSVS just search for suitable constructor before conversions operator, but then found that it starts calling A's operator B() if you remove const word from B's constructor. Probably it has some special behavior for temporaries, because the following code still calls B's constructor:

A a;

B b = a;
Disused answered 5/9, 2009 at 19:27 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.