13

I have two classes, one of which, say, represents a string, and the other can be converted to a string:

class A {
public:
  A() {}
  A(const A&) {}
  A(const char*) {}

  A& operator=(const A&) { return *this; }
  A& operator=(const char*) { return *this; }

  char* c;
};
class B {
public:
  operator const A&() const {
    return a;
  }
  operator const char*() const {
    return a.c;
  }

  A a;
};

Now, if I do

B x;
A y = x;

It triggers copy constructor, which compiles fine. But if I do

A y;
y = x;

It complains about ambiguous assignment, and can't choose between =(A&) and =(char*). Why the difference?

1
  • 4
    Madness. That's all I can say about the deepest pits of C++.... Commented May 27, 2015 at 11:27

2 Answers 2

5

There is a difference between initialization and assignment.

In initialization, that is:

A y = x;

The actual call depends on the type of x. If it is the same type of y, then it will be like:

A y(x);

If not, as in your example, it will be like:

A y(static_cast<const A&>(x));

And that compiles fine, because there is no ambiguity any more.

In the assignment there is no such special case, so no automatic resolution of the ambiguity.

It is worth noting that:

A y(x);

is also ambiguous in your code.

Sign up to request clarification or add additional context in comments.

2 Comments

It might be worthwhile pointing out that there are some simplifications in this description. For example, the static_cast is actually an implicit conversion, which allows no explicit conversion functions.
@dyp True. For an exact but dense explanation, there is the Columbo answer.
4

There is §13.3.1.4/(1.2), only appertaining to (copy-)initialization of objects of class type, that specifies how candidate conversion functions for your first case are found:

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. 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 non-explicit conversion functions of S and its base classes are considered. When initializing a temporary to be bound to the first parameter of a constructor where the parameter is of type “reference to possibly cv-qualified T” and the constructor is called with a single argument in the context of direct-initialization of an object of type “cv2 T”, explicit conversion functions are also 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 or xvalues, depending on the type of reference, of type X and are therefore considered to yield X for this process of selecting candidate functions.

I.e. operator const char* is, though being considered, not included in the candidate set, since const char* is clearly not similar to A in any respect. However, in your second snippet, operator= is called as an ordinary member function, which is why this restriction doesn't apply anymore; Once both conversion functions are in the candidate set, overload resolution will clearly result in an ambiguity.

Note that for direct-initialization, the above rule doesn't apply either.

B x;
A y(x);

Is ill-formed.

A more general form of this result is that there can never be two user-defined conversions in one conversion sequence during overload resolution. Consider §13.3.3.1/4:

However, if the target is

  • the first parameter of a constructor or […]

and the constructor […] is a candidate by

  • 13.3.1.3, when the argument is the temporary in the second step of a class copy-initialization, or
  • 13.3.1.4, 13.3.1.5, or 13.3.1.6 (in all cases),

user-defined conversion sequences are not considered. [Note: These rules prevent more than one user-defined conversion from being applied during overload resolution, thereby avoiding infinite recursion. — end note ]

3 Comments

TL;DR: The Standard is written such that there are never two user-defined conversions in a conversion sequence.
@dyp Ah, so you're saying that the conversion sequence does not include the call to the final constructor called - but it does include the call to the implied temporary's constructor that the object is initialized with?
T t = x; is the definition of an implicit conversion. Let X be the type of x. We can only allow a conversion (function) from X directly to T OR a constructor of T that accepts an X. Otherwise, there would be an intermediary conversion from X to U to T. An initialization T t(x); is not an implicit conversion, I wonder if it's considered a conversion at all.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.