Which of two conversion operators must be selected by C++ compiler? - c++

A class can declare several conversion operators. In particular it can be conversion operators to some type and to const-reference of the same type. Which of the two conversion operators must be selected in case of requested conversion to that type?
Consider an example:
#include <iostream>
struct B {};
static B sb;
struct A {
operator B() { std::cout << "operator B() "; return sb; }
operator const B &() { std::cout << "operator const B &() "; return sb; }
};
int main() {
A a;
[[maybe_unused]] B b(a);
}
Here Clang selects operator B(), MSVC selects operator const B &(), and GCC complains about ambiguity of the selection:
<source>:13:27: error: call of overloaded 'B(A&)' is ambiguous
13 | [[maybe_unused]] B b(a);
| ^
<source>:3:8: note: candidate: 'constexpr B::B(const B&)'
3 | struct B {};
| ^
<source>:3:8: note: candidate: 'constexpr B::B(B&&)'
Demo: https://gcc.godbolt.org/z/874h7h3d1
Which of the compilers is right?

The program is ill-formed and rejected by GCC is correct here but the diagnosis can arguably say it is not completely correct. For this declaration B b(a);, it is direct-initialization of an object of class B from the initializer a of type A, according to [over.match.copy] p1
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 of T are candidate functions.
When the type of the initializer expression is a class type “cv S”, conversion functions are considered. The permissible types for non-explicit conversion functions are T and any class derived from T. When initializing a temporary object ([class.mem]) to be bound to the first parameter of a constructor where the parameter is of type “reference to cv2 T” and the constructor is called with a single argument in the context of direct-initialization of an object of type “cv3 T”, the permissible types for explicit conversion functions are the same; otherwise there are none.
For converting constructors, they are copy/move constructors of B, however, [over.best.ics#general-4] prohibits the user-defined conversion sequence to apply to the target to match the parameter of the constructor
However, if the target is
the first parameter of a constructor or
[...]
and the constructor or user-defined conversion function is a candidate by
[...]
[over.match.copy], [over.match.conv], or [over.match.ref] (in all cases), or
[...]
user-defined conversion sequences are not considered.
Hence, the copy/move constructors of B are not viable functions. The ambiguity arises from the viable functions A::operator B() and A::operator const B &(), since the implicit parameter objects of them both have type A& and the corresponding argument is an lvalue of type A, hence neither is better than the other. Hence, the only opportunity that can determine which is better falls on [over.match.best#general-2.2]
the context is an initialization by user-defined conversion (see [dcl.init], [over.match.conv], and [over.match.ref]) and the standard conversion sequence from the return type of F1 to the destination type (i.e., the type of the entity being initialized) is a better conversion sequence than the standard conversion sequence from the return type of F2 to the destination type.
The second standard conversion sequences of them are both identity conversions, hence they are not indistinguishable. So, the result is ambiguity. GCC is merely correct in that the program is ambiguous, but, obviously, its diagnosis has a bit misleading. Since the copy/move constructors are not viable functions in this case at all, how could they cause the ambiguity? If we suppress the production of the defaulted move constructor, GCC and Clang are both incorrect here, which is back to this question you have referred.

Related

What's the rank of implicitly conversion for copy-list-initialization

#include <iostream>
struct A{
A(int){
}
};
struct B{
B() = default;
B(A){
}
B(B const&){}
B(B&&){}
};
int main(){
B b({0});
}
For the given codes, the candidate functions are:
#1 B::B(A)
#2 B::B(const B&)
#3 B::B(B&&)
According to the standard, for #1, the object of type A is copy-list-initialized by {0} as A a = {0}, A::A(int) is considered for the initialization, so only the standard conversion within #1. For #2, it's an initialization of a reference form braced-init-list which is the cause of [dcl.init.list]
Otherwise, if T is a reference type, a prvalue of the type referenced by T is generated. The prvalue initializes its result object by copy-list-initialization or direct-list-initialization, depending on the kind of initialization for the reference. The prvalue is then used to direct-initialize the reference. [ Note: As usual, the binding will fail and the program is ill-formed if the reference type is an lvalue reference to a non-const type.  — end note ]
So it equates with const B& = {0}, in this initialization, the conversion function is B::B(A) and the argument is 0, so B tmp = {0} and 'B::B(A)' is considered that parameter is initialized by argument 0, as A parameter = 0.
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 [over.match.copy], and the best one is chosen through overload resolution...
So there's a user-defined conversion within #2 and the situation of #3 is the same as that of #2 and accroding to the [over.ics.rank],
a standard conversion sequence is a better conversion sequence than a user-defined conversion sequence or an ellipsis conversion sequence, and...
The standard conversion is better than user-defined conversion, so #1 should be better than #2 and #3, but actually, g++ report the invocation is ambiguous, why? The error message is:
main.cpp: In function ‘int main()’:
main.cpp:12:10: error: call of overloaded ‘B(<brace-enclosed initializer list>)’ is ambiguous
B b({0});
^
main.cpp:8:3: note: candidate: B::B(A)
B(A){
^
main.cpp:6:8: note: candidate: constexpr B::B(const B&)
struct B{
^
main.cpp:6:8: note: candidate: constexpr B::B(B&&)
All the three conversions {0} -> A, {0} -> const B&, {0} -> B&& are user-defined conversions.
To convert {0} to A, another overload resolution happens and this time you face three constructors A(int), A(const A&) and A(A&&). Since 0 -> int is a standard conversion while both 0 -> const A& and 0 -> A&& are user-defined conversion, the conversion 0 -> int wins and A(int) is selected to convert {0} to A.
Your confusion comes from mixing the two overload resolutions.
Answers are here,Because the argument is initializer list,so [over.ics.list] rules are performed when overload resolution.
[over.ics.list]/6
Otherwise, if the parameter is a non-aggregate class X and overload resolution per [over.match.list] chooses a single best constructor C of X to perform the initialization of an object of type X from the argument initializer list:
If C is not an initializer-list constructor and the initializer list has a single element of type cv U, where U is X or a class derived from X, the implicit conversion sequence has Exact Match rank if U is X, or Conversion rank if U is derived from X.
Otherwise, the implicit conversion sequence is a user-defined conversion sequence with the second standard conversion sequence an identity conversion.
Hence,for these three candiate constructors,the parameter of them are all non-aggerate class type,so the implicit conversion sequence is used to convert the single element of the initializer list to corresponding parameter type of the constructor of parameter of constructor B and therefore these conversion sequences are all user-defined conversion sequence and the second standard conversion sequence are all identity conversions.So the rank of them are indistinguishable.

C++17: explicit conversion function vs explicit constructor + implicit conversions - have the rules changed?

Clang 6, clang 7, and gcc 7.1, 7.2, and 7.3 all agree that the following is valid C++17 code, but is ambiguous under C++14 and C++11. MSVC 2015 and 2017 accept it as well. However, gcc-8.1 and 8.2 reject it even in c++17 mode:
struct Foo
{
explicit Foo(int ptr);
};
template<class T>
struct Bar
{
operator T() const;
template<typename T2>
explicit operator T2() const;
};
Foo foo(Bar<char> x)
{
return (Foo)x;
}
The compilers that accept it pick the templated explicit conversion function, Bar::operator T2().
The compilers that reject it agree that there is an ambiguity between:
the explicit conversion function Bar::operator int()
first using the implicit user-defined conversion from Bar<char> to char, then the implicit built-in conversion from char to int, and then the explicit constructor Foo(int).
So, which compiler is right? What is the relevant difference in the standard between C++14 and C++17?
Appendix: actual error messages
Here's the error for gcc-8.2 -std=c++17. gcc-7.2 -std=c++14 prints the same error:
<source>: In function 'Foo foo(Bar<char>)':
<source>:17:17: error: call of overloaded 'Foo(Bar<char>&)' is ambiguous
return (Foo)x;
^
<source>:3:14: note: candidate: 'Foo::Foo(int)'
explicit Foo(int ptr);
^~~
<source>:1:8: note: candidate: 'constexpr Foo::Foo(const Foo&)'
struct Foo
^~~
<source>:1:8: note: candidate: 'constexpr Foo::Foo(Foo&&)'
And here's the error from clang-7 -std=c++14 (clang-7 -std=c++17 accepts the code):
<source>:17:12: error: ambiguous conversion for C-style cast from 'Bar<char>' to 'Foo'
return (Foo)x;
^~~~~~
<source>:1:8: note: candidate constructor (the implicit move constructor)
struct Foo
^
<source>:1:8: note: candidate constructor (the implicit copy constructor)
<source>:3:14: note: candidate constructor
explicit Foo(int ptr);
^
1 error generated.
There are several forces at play here. To understand what's happening, let's examine where (Foo)x should lead us. First and foremost, that c-style cast is equivalent to a static_cast in this particular case. And the semantics of the static cast would be to direct-initialize the result object. Since the result object would be of a class type, [dcl.init]/17.6.2 tells us it's initialized as follows:
Otherwise, 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. The applicable constructors
are enumerated ([over.match.ctor]), and the best one is chosen through
overload resolution. The constructor so selected is called to
initialize the object, with the initializer expression or
expression-list as its argument(s). If no constructor applies, or the
overload resolution is ambiguous, the initialization is ill-formed.
So overload resolution to pick the constructor of Foo to call. And if overload resolution fails, the program is ill-formed. In this case, it shouldn't fail, even though we have 3 candidate constructors. Those are Foo(int), Foo(Foo const&) and Foo(Foo&&).
For the first ,we need to copy initialize an int as an argument to the constructor, and that means find an implicit conversion sequence from Bar<char> to int. Since the user defined conversion operator you provided from Bar<char> to char is not explicit, we can use it to from an implicit conversation sequence Bar<char> -> char -> int.
For the other two constructors, we need to bind a reference to a Foo. However, we cannot do that. According to [over.match.ref]/1 :
Under the conditions specified in [dcl.init.ref], a reference can be
bound directly to a glvalue or class prvalue that is the result of
applying a conversion function to an initializer expression. Overload
resolution is used to select the conversion function to be invoked.
Assuming that “cv1 T” is the underlying type of the reference being
initialized, and “cv S” is the type of the initializer expression,
with S a class type, the candidate functions are selected as follows:
The conversion functions of S and its base classes are considered. Those non-explicit conversion functions that are not hidden within S
and yield type “lvalue reference to cv2 T2” (when initializing an
lvalue reference or an rvalue reference to function) or “ cv2 T2” or
“rvalue reference to cv2 T2” (when initializing an rvalue reference or
an lvalue reference to function), where “cv1 T” is
reference-compatible ([dcl.init.ref]) with “cv2 T2”, are candidate
functions. For direct-initialization, those explicit conversion
functions that are not hidden within S and yield type “lvalue
reference to cv2 T2” or “cv2 T2” or “rvalue reference to cv2 T2,”
respectively, where T2 is the same type as T or can be converted to
type T with a qualification conversion ([conv.qual]), are also
candidate functions.
The only conversion function that can yield us a glvalue or prvalue of type Foo is a specialization of the explicit conversion function template you specified. But, because initialization of function arguments is not direct initialization, we cannot consider the explicit conversion function. So we cannot call the copy or move constructors in overload resolution. That leaves us only with the constructor taking an int. So overload resolution is a success, and that should be it.
Then why do some compilers find it ambiguous, or call the templated conversion operator instead? Well, since guaranteed copy elision was introduced into the standard, it was noted (CWG issue 2327) that user defined conversion functions should also contribute to copy elision. Today, according to the dry letter of the standard, they do not. But we'd really like them to. While the wording for exactly how it should be done is still being worked out, it would seem that some compilers already go ahead and try to implement it.
And it's that implementation that you see. It's the opposing force of extending copy elision that interferes with overload resolution here.

What rules govern use of multiple user-defined conversions between types?

I have this code:
class MyString
{
public:
operator const char*() const {
return nullptr;
}
};
class YourString
{
public:
YourString() {}
YourString(const char* ptr) {
(void)ptr;
}
YourString& operator=(const char* ptr)
{
return *this;
}
};
int main()
{
MyString mys;
YourString yoursWorks;
yoursWorks = mys;
YourString yoursAlsoWorks(mys);
YourString yoursBreaks = mys;
}
MSVC accepts it without issue. Clang-CL does not accept it:
$ "C:\Program Files\LLVM\msbuild-bin\CL.exe" ..\string_conversion.cpp
..\string_conversion.cpp(32,13): error: no viable conversion from 'MyString' to 'YourString'
YourString yoursBreaks = mys;
^ ~~~
..\string_conversion.cpp(10,7): note: candidate constructor (the implicit copy constructor) not viable: no known conversion from 'MyString' to
'const YourString &' for 1st argument
class YourString
^
..\string_conversion.cpp(10,7): note: candidate constructor (the implicit move constructor) not viable: no known conversion from 'MyString' to
'YourString &&' for 1st argument
class YourString
^
..\string_conversion.cpp(14,2): note: candidate constructor not viable: no known conversion from 'MyString' to 'const char *' for 1st argument
YourString(const char* ptr) {
^
..\string_conversion.cpp(5,2): note: candidate function
operator const char*() const {
^
1 error generated.
Nor does GCC:
$ g++.exe -std=gnu++14 ..\string_conversion.cpp
..\string_conversion.cpp: In function 'int main()':
..\string_conversion.cpp:33:27: error: conversion from 'MyString' to non-scalar type 'YourString' requested
YourString yoursBreaks = mys;
^
I understand that only one user-defined conversion is allowed.
However, is MSVC justified in treating the line
YourString yoursBreaks = mys;
as
YourString yoursBreaks(mys);
and accepting it? Is that a conversion compilers are allowed to do? Under what rules is it allowed/disallowed? Is there a similar rule?
Update: With MSVC, the /Za flag causes the code to not be accepted.
$ "C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\bin\x86_amd64\CL.exe" /Za ..\string_conversion.cpp
string_conversion.cpp
..\string_conversion.cpp(33): error C2440: 'initializing': cannot convert from 'MyString' to 'YourString'
..\string_conversion.cpp(33): note: No user-defined-conversion operator available that can perform this conversion, or the operator cannot be called
tldr; The code is ill-formed, MSVC is wrong to accept it. Copy-initialization is different from direct-initialization. The layman explanation is that the initialization of yoursBreaks would involve two user-defined conversions (MyString --> const char* --> YourString), whereas direct-initialization involves one user-defined conversion (MyString --> const char*), and you are allowed at most one user-defined conversion. The standardese explanation which enforces that rule is that [over.best.ics] doesn't allow for user-defined conversions in the context of copy-initialization of a class type from an unrelated class type by way of converting constructor.
To the standard! What does:
YourString yoursBreaks = mys;
mean? Any time we declare a variable, that's some kind of initialization. In this case, it is, according to [dcl.init]:
The initialization that occurs in the = form of a brace-or-equal-initializer or condition (6.4), as well as in argument passing, function return, throwing an exception (15.1), handling an exception (15.3), and aggregate member initialization (8.6.1), is called copy-initialization.
Copy-initialization is anything of the form T var = expr; Despite the appearance of the =, this never invokes operator=. We always goes through either a constructor or a conversion function.
Specifically, this case:
If the destination type is a (possibly cv-qualified) class type:
— If the initializer expression is a prvalue and the cv-unqualified version of the source type is the same
class as the class of the destination, [...]
— Otherwise, 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, [...]
— 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). If the conversion cannot be done or is ambiguous, the initialization is
ill-formed.
We fall into that last bullet. Let's hop over into 13.3.1.4:
— 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.
The first bullet point gives us the converting constructors of YourString, which are:
YourString(const char* );
The second bullet gives us nothing. MyString does not have a conversion function that returns YourString or a class type derived from it.
So, okay. We have one candidate constructor. Is it viable? [over.match] checks reliability via:
Then the best viable function is selected based on the implicit conversion sequences (13.3.3.1) needed to match each argument to the corresponding parameter of each viable function.
and, in [over.best.ics]:
A well-formed implicit conversion sequence is one of the following forms:
— a standard conversion sequence (13.3.3.1.1),
— a user-defined conversion sequence (13.3.3.1.2), or
— an ellipsis conversion sequence (13.3.3.1.3).
However, if the target is
— the first parameter of a constructor or
— the implicit object parameter of a user-defined conversion function
and the constructor or user-defined conversion function is a candidate by
— 13.3.1.3, when the argument is the temporary in the second step of a class copy-initialization,
— 13.3.1.4, 13.3.1.5, or 13.3.1.6 (in all cases), or
— the second phase of 13.3.1.7 [...]
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 ] [ Example:
struct Y { Y(int); };
struct A { operator int(); };
Y y1 = A(); // error: A::operator int() is not a candidate
struct X { };
struct B { operator X(); };
B b;
X x({b}); // error: B::operator X() is not a candidate
—end example ]
So even though there is a conversion sequence from MyString to const char*, it is not considered in this case, so this constructor is not viable.
Since we don't have another candidate constructor, the call is ill-formed.
The other line:
YourString yoursAlsoWorks(mys);
is called direct-initialization. We call into the 2nd bullet point of the three in the [dcl.init] block I quoted earlier, which in its entirety reads:
The applicable constructors are enumerated (13.3.1.3),
and the best one is chosen through overload resolution (13.3). The constructor so selected is called to initialize the object, with the initializer expression or expression-list as its argument(s). If no constructor applies, or the overload resolution is ambiguous, the initialization is ill-formed.
where 13.3.1.3 indicates that constructors are enumerated from:
For direct-initialization
or default-initialization that is not in the context of copy-initialization, the candidate functions are all the constructors of the class of the object being initialized.
Those constructors are:
YourString(const char* ) // yours
YourString(YourString const& ) // implicit
YourString(YourString&& ) // implicit
To check the viability of the latter two functions, we re-perform overload resolution from a copy-initialization context (which fails as per the above). But for your YourString(const char*), it's straightforward, there is a viable conversion function from MyString to const char*, so it's used.
Note that there is one single conversion here: MyString --> const char*. One conversion is fine.
Let's look at the rules for implicit conversions found here. The interesting bit is this :
Implicit conversions are performed whenever an expression of some type T1 is used in context that does not accept that type, but accepts some other type T2; in particular: [...]when initializing a new object of type T2[...]
And
A user-defined conversion consists of zero or one non-explicit single-argument constructor or non-explicit conversion function call
Case 1
YourString yoursWorks;
yoursWorks = mys;
In the first case, we need one non-explicit conversion function call. YourString::operator= expects const char* and is given a MyString. MyString provides a the non-explicit conversion function for this conversion.
Case 2
YourString yoursAlsoWorks(mys);
In the second case, we again need one non-explicit conversion function call. YourString::YourString expects const char* and is given a MyString. MyString provides a the non-explicit conversion function for this conversion.
Case 3
YourString yoursBreaks = mys;
The third case is different because it's not an assignment copy as it would appear. Contrary to the second case, yoursBreaks has not been initialized yet. You cannot call the assignment operator operator= on an object that hasn't been constructed yet. It's in fact an assignment by copy construction. To assign mys to yoursBreaks we need both a non-explicit conversion function call (to convert mys to const char* and then a non-explicit single-argument constructor (to construct the YourString from a const char *. Implicit conversions only allow for one or the other.
First of all
YourString yoursWorks;
yoursWorks = mys;
is not equivalent to
YourString yoursAlsoWorks(mys);
or to
YourString yoursBreaks = (const char*) mys;
The first approach uses the constructor
YourString() {}
followed by MyString's conversion operator, and YourString's
assignment operator.
The second approach uses the constructor
YourString(const char* ptr) {(void)ptr;}
and MyString's conversion operator.
(This might be demonstrated by adding trace statements to the constructors.)
Then, when the (const char*) cast is missing from the last statement,
MSVC will assume that it should be added implicitly.
While this looks a reasonable approach, it conflicts the description
in Stroustrup's book The C++ Programming Language 4th edition:
18.4 Type Conversion
...
explicit, that is, the conversion is only performed in a direct initialization i.e., as an initializer not using
a =.
...

Reference binding through ambiguous conversion operator

#include <iostream>
using namespace std;
struct CL2
{
CL2(){}
CL2(const CL2&){}
};
CL2 cl2;
struct CL1
{
CL1(){}
operator CL2&(){cout<<"operator CL2&"; return cl2;}
operator const CL2&(){cout<<"operator const CL2&"; return cl2;}
};
CL1 cl1;
int main()
{
CL1 cl1;
CL2 cl2 (cl1);
}
Both clang and gcc give ambiguous conversion operator, but Visual Studio compiles ok and prints "operator const CL2&". How must be right according to Standard?
As I undestand, conversion of CL1 to const CL2& is in copy-initialization context (as a part of a direct-initialization of cl2 object). I seen n4296 draft, [over.match.copy]:
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. both of conversion operators are considered as return CL2 and const CL2 (not just CL2 without const) and it remains to solve, which conversion is better: CL2 -> const CL2& or const CL2 -> const CL2&. The second case seems more appropriate. Should a better qualification conversion considered in that context? Or both cases are Identity conversion? I couldn't find it in Standard
Since both conversion operators have identical signatures, the only way in which one could be preferred over the other is by application of [over.match.best]/(1.4)…
— the context is an initialization by user-defined conversion (see 8.5,
13.3.1.5, and 13.3.1.6) and the standard conversion sequence from the return type of F1 to the destination type (i.e., the type of the
entity being initialized) is a better conversion sequence than the
standard conversion sequence from the return type of F2 to the
destination type.
…or (1.5):
— the context is an initialization by conversion function for direct
reference binding (13.3.1.6) of a reference to function type, […]
Clearly, neither applies, hence the ambiguity. A possible way to disambiguate:
operator CL2&();
operator const CL2&() const;
Demo; Here, the former overload's initial standard conversion sequence of the implicit object argument is better as per [over.ics.rank]/(3.2.6), which is decisive by [over.match.best]/(1.3).

Ambiguous assignment operator

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?
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.
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 ]