views:

736

answers:

2

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.

Thanks in advance

+7  A: 

Hello there. 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 fuction. 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.
Johannes Schaub - litb
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.
GRB
What a coincidence that i put the same "analysis" on another answer of mine as an example earlier: http://stackoverflow.com/questions/1051379/is-there-a-difference-in-c-between-copy-initialization-and-assignment-initializ/1051468#1051468 xD
Johannes Schaub - litb
+1  A: 

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;
Xeor