3
votes

I have the following code snippet:

struct T {
    T(const T&) = default;
    T(const S &);
};

struct S {
    operator T();
};

int main() {
    S s;
    T t = s; // copy-initialization of class type
    return 0;
}

My question is why the compiler prefers S::operator T() for the initialization of t rather than reporting an error that the initialization is ambigious. In my opinion the following happens (correct me if i am wrong):

  • t is copy-initialized with an lvalue of type S
  • S is not T and S is also not a subclass of T, so S and T are unrelated
  • because of the fact that the variable t is copy-initialized and the fact that the types S and T are unrelated, the compiler tries to find user-defined-conversion sequences to do the initialization.
  • overload resolution is responsible for selecting the best user-defined-conversion which can be either a converting constructor of T or the conversion function of S
  • the implicite conversion sequence for the constructor T::T(const S&) from the argument s is the identity conversion because the lvalue s can be bound directly to this lvalue reference
  • the implicite conversion sequence for the conversion function S::operator T() from the argument s is also the identity conversion, because the implicit object parameter is S&

Both the constructor and the conversion function return a prvalue of type T which can be used to direct-initialize the variable t. That means that the second standard conversion sequence of both user-defined-conversion sequences is the identity conversion.

This would mean that both user-defined-conversion sequences are equally good. Or is there a special rule which prefers the conversion functions?

I was reading the following rules in the c++11 standard:

The initialization that occurs in the form T x = a; as well as in argument passing, function return, throwing an exception (15.1), handling an exception (15.3), and aggregate member initialization (8.5.1) is called copy-initialization.

The semantics of initializers are as follows...If the destination type is a (possibly cv-qualified) class type: If the initialization is direct-initialization, or if it is copy-initialization where the cv-unqualified version of the source type is the same class as, or a derived class of, the class of the destination, constructors are considered.... 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)

User-defined conversion sequence U1 is a better conversion sequence than another user defined conversion sequence U2 if they contain the same user-defined conversion function or constructor and if the second standard conversion sequence of U1 is better than the second standard conversion sequence of U2

Maybe i am making false assumptions. I hope you can help me!

2

2 Answers

2
votes

The conversion from S using the conversion operator is better than the conversion to T taking an S const as argument. If you make s an S const, the constructor is preferred: Your identity operation in one case, indeed, is an identity operation, in the other case it isn't. If you make the conversion operator of S a const member, you get an ambiguity. Below is a test program demonstrating all cases:

struct S;
struct T {
    T(const T&) = default;
    T(const S &);
};

struct S {
    S(); // needed to allow creation of a const object
#ifdef AMBIGUOUS
    operator T() const;
#else
    operator T();
#endif
};

int main() {
#ifdef CONST
    S const s;
#else
    S s;
#endif
    T t = s; // copy-initialization of class type
    return 0;
}
0
votes

I think i found the rule which clarifies this:

13.3.3.2: ... 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.

In the member function S::operator T() the implicit object parameter has type S& which is directly bound to the lvalue s of type S. In the constructor T::T(const S&) the parameter is directly bound to the lvalue s of type S but this reference binding is more cv-qualified than in the operator function, so the operator function is preferred by overload resolution.

Do you agree with this?