Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Shouldn't this code throw an ambiguous conversion error?

Tags:

c++

casting

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
}
like image 549
roro Avatar asked Feb 16 '16 06:02

roro


Video Answer


1 Answers

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).

like image 190
Niall Avatar answered Oct 17 '22 06:10

Niall