15

I have two classes, A and B, each defining a conversion to B. A has a conversion operator to B, B has a constructor from A. Shouldn't a call to static_cast<B> be ambiguous? Using g++ this code compiles and chooses the conversion constructor.

#include<iostream>

using namespace std;

struct B;
struct A {
    A(const int& n) : x(n) {}
    operator B() const;         //this const doesn't change the output of this code
    int x;
};

struct B{
    B(const double& n) : x(n) {}
    B(const A& a);
    double x;
};

A::operator B() const           //this const doesn't change the output of this code
{
    cout << "called A's conversion operator" << endl;
    return B(double(x));
}

B::B(const A& a)
{
    cout << "called B's conversion constructor" << endl;
    x = (double) a.x;
}

int main() {
    A a(10);
    static_cast<B>(a);            // prints B's conversion constructor
}
Niall
  • 30,036
  • 10
  • 99
  • 142
roro
  • 931
  • 5
  • 21
  • Interesting. I've updated the code to test that answer.That answer seems to imply the conversion operator should be called, whereas this code calls the conversion constructor. – roro Feb 16 '16 at 07:23
  • 2
    I think you're correct. The questions are pretty similar, so they look like dupes, but they're not. – Weak to Enuma Elish Feb 16 '16 at 07:39

1 Answers1

13

For user defined conversion sequences; there does not seem to a precedence given between the converting constructor and the conversion operator, they are both candidates;

§13.3.3.1.2/1 User-defined conversion sequences

A user-defined conversion sequence consists of an initial standard conversion sequence followed by a user- defined conversion (12.3) followed by a second standard conversion sequence. If the user-defined conversion is specified by a constructor (12.3.1), the initial standard conversion sequence converts the source type to the type required by the argument of the constructor. If the user-defined conversion is specified by a conversion function (12.3.2), the initial standard conversion sequence converts the source type to the implicit object parameter of the conversion function.

Hence if the conversion had been;

B b2 = a; // ambiguous?

It could be ambiguous and the compilation fail. Clang fails the compilation, g++ accepts the code and uses the constructor; demo code, VS also accepts the code. VS and g++ call the converting constructor (as per the OP code).

In consideration of the posted code, the user defined conversion sequences (by constructor and converting operator) and the use of static_cast need to be considered.

§5.2.9/4 Static cast

An expression e can be explicitly converted to a type T using a static_cast of the form static_cast<T>(e) if the declaration T t(e); is well-formed, for some invented temporary variable t (8.5). The effect of such an explicit conversion is the same as performing the declaration and initialization and then using the temporary variable as the result of the conversion. The expression e is used as a glvalue if and only if the initialization uses it as a lvalue.

From the above quote, the static_cast is equivalent to B temp(a); and as such, the direct initialisation sequence is used.

§13.3.1.3/1 Initialization by constructor

When objects of class type are direct-initialized (8.5), copy-initialized from an expression of the same or a derived class type (8.5), or default-initialized (8.5), overload resolution selects the constructor. For direct- initialization or default-initialization, the candidate functions are all the constructors of the class of the object being initialized. For copy-initialization, the candidate functions are all the converting constructors (12.3.1) of that class. The argument list is the expression-list or assignment-expression of the initialiser.

In general (excluding any constructors and operators marked as explicit and const concerns), given the B(const A& a); constructor and the construction of a B from an A, the constructor should win since it offers the exact match when considering the best viable function; since further implicit conversions are not needed (§13.3; Overload resolution).


If the constructor B(const A& a); was removed, the conversion (with the static_cast<> would still succeed since the user defined conversion operator is a candidate and its use is not ambiguous.

§13.3.1.4/1 Copy-initialization of class by user-defined conversion

Under the conditions specified in 8.5, as part of a copy-initialization of an object of class type, a user-defined conversion can be invoked to convert an initializer expression to the type of the object being initialized.

Quotes are taken from the N4567 draft of the C++ standard.


It would also be instructive to invoke a user-defined conversion sequence outside just the construction of an object, i.e. calling a method.

Given the code listing (and the rules above);

#include <iostream>
using namespace std;
struct A;
struct B {
    B() {}
    B(const A&) { cout << "called B's conversion constructor" << endl; }
};
struct A {
    A() {}
    operator B() const { cout << "called A's conversion operator" << endl; return B(); }
};
void func(B) {}
int main() {
    A a;
    B b1 = static_cast<B>(a); // 1. cast
    B b2 = a; // 2. copy initialise
    B b3 ( a ); // 3. direct initialise
    func(a); // 4. user defined conversion
}

Clang, g++ (demo) and VS offer different results and thus possibly different levels of compliance.

  • clang fails 2. and 4.
  • g++ accepts 1. through 4.
  • VS fails 4.

From the rules above, 1. through 3. should all succeed since the B converting constructor is a candidate and requires no further user conversions; direct construction and copy initialisation is used for those forms. Reading from the standard (the excerpts above, in particular §13.3.3.1.2/1 and §13.3.1.4/1, and then §8.5/17.6.2), 2. and 4. could/should fail and be ambiguous - since the conversion constructor and the conversion operator are being considered with no clear ordering.

I believe this may well be an unintended use case (types being able to convert to each other in this way; there is an argument for where there would be one conversion sequence would be the general use case).

Niall
  • 30,036
  • 10
  • 99
  • 142
  • I think 2 and 4 should fail, as they are *copy-initialization* (but not from an expression of the same type - so covered by [over.match.copy], not [over.match.ctor]) and there are two user-defined sequences. (Two user-defined sequences is always ambiguous unless it is identical user-defined conversions combined with differently-ranked standard conversions) – M.M Feb 16 '16 at 22:00
  • @M.M Possible, but [over.match.ctor] explicitly says "For copy-initialization, the candidate functions are all the converting constructors (12.3.1) of that class" I think both interpretations could be applied. – Niall Feb 17 '16 at 05:47
  • [over.match.ctor] says "copy-initialized from an expression of the same or a derived class type " . (The rest of the paragraph applies only to the cases selected by the first sentence) – M.M Feb 17 '16 at 11:04
  • @M.M I see what you saying. I've read further into [dcl.init]/17.6.2 (§8.5/17.6 and 17.6.2) as well, they make more sense to me when read together (8.5 and 13.3). I'll need to fold that into the answer as well. It does make 2. and 4. ambiguous as you say. – Niall Feb 17 '16 at 13:07