Conversion operator in direct-initialization - c++

The C++14 standard (N4296) says in 8.5/17.6.1
If the initialization is direct-initialization [...], constructors are considered. The applicable constructors are enumerated, and the best
one is chosen through overload resolution. [...] If no constructor
applies, or the overload resolution is ambiguous, the initialization is ill-formed.
Therefore in direct-initialization, only constructors are considered - conversion functions are ignored. In the following code there is no applicable constructor of A, only a conversion function from B. However, the code compiles, why?
struct A{};
struct B{
operator A(){ return A{}; }
};
int main() {
B b;
A a(b); // direct-initialization
}

You are correct that only the constructors of A are considered when doing A a(b);. [over.match.ctor]/1 states
When objects of class type are direct-initialized, copy-initialized from an expression of the same or a derived class type ([dcl.init]), or default-initialized, overload resolution selects the constructor. 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. For copy-initialization (including default initialization in the context of copy-initialization), the candidate functions are all the converting constructors ([class.conv.ctor]) of that class. The argument list is the expression-list or assignment-expression of the initializer.
emphasis mine
This means that A(), A(const A&) and A(A&&) are the candidate list. Then we have [over.match.viable]/4
[...]Third, for F to be a viable function, there shall exist for each argument an implicit conversion sequence that converts that argument to the corresponding parameter of F.[..]
which allows an implicit conversion of b to an A so that A(A&&) can be called.

Related

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

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.

constexpr array of constexpr objects using move ctor

I have a class with a constexpr value constructor, but no copy or move ctor
class C {
public:
constexpr C(int) { }
C(const C&) = delete;
C& operator=(const C&) = delete;
};
int main() {
constexpr C arr[] = {1, 2};
}
I've found that this code doesn't work because it's actually trying to use the move constructor for C rather than the value constructor to construct in place. One issue is that I want this object to be unmovable (for test purposes) but I thought "okay, fine, I'll add a move constructor."
class C {
public:
constexpr C(int) { }
C(const C&) = delete;
C& operator=(const C&) = delete;
C& operator=(C&&) = delete;
C(C&&) { /*something*/ } // added, assume this must be non trivial
};
Okay fine, now it uses the move constructor and everything works under gcc but when I use clang, it complains because the move constructor is not marked constexpr
error: constexpr variable 'arr' must be initialized by a constant expression
constexpr C arr[] = {1, 2};
If I mark the move constructor constexpr it works under gcc and clang, but the issue is that I want to have code in the move constructor if it runs at all, and constexpr constructors must have empty bodies. (The reason for my having code in the move ctor isn't worth getting into).
So who is right here? My inclination is that clang would be correct for rejecting the code.
NOTE
It does compile with initializer lists and non-copyable non-movable objects as below:
class C {
public:
constexpr C(int) { }
C(const C&) = delete;
C& operator=(const C&) = delete;
C& operator=(C&&) = delete;
C(C&&) = delete;
};
int main() {
constexpr C arr[] = {{1}, {2}};
}
My main concern is which compiler above is correct.
So who is right here?
Clang is correct in rejecting the code. [expr.const]/2:
A conditional-expression e is a core constant expression unless
the evaluation of e, following the rules of the abstract machine
(1.9), would evaluate one of the following expressions:
an invocation of a function other than a constexpr constructor for a literal class, a constexpr function, or an implicit invocation
of a trivial destructor (12.4)
Clearly your move constructor isn't a constexpr constructor - [dcl.constexpr]/2
Similarly, a constexpr specifier used in a constructor declaration
declares that constructor to be a constexpr constructor.
And the requirements for an initializer of a constexpr object are in [dcl.constexpr]/9:
[…] every full-expression that appears in its initializer shall be a
constant expression. [ Note: Each implicit conversion used in
converting the initializer expressions and each constructor call used
for the initialization is part of such a full-expression. — end note
]
Finally the move constructor is invoked by the copy-initialization of the array elements with the corresponding initializer-clauses - [dcl.init]:
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. The function selected is called with the initializer
expression as its argument; if the function is a constructor, the
call initializes a temporary of the cv-unqualified version of the
destination type. The temporary is a prvalue. The result of the call
(which is the temporary for the constructor case) is then used to
direct-initialize, according to the rules above, the object that is
the destination of the copy-initialization.
In the second example, copy-list-initialization applies - and no temporary is introduced.
By the way: GCC 4.9 does not compile the above, even without any warning flags provided.
§8.5 [dcl.init]/p17:
The semantics of initializers are as follows. The destination type is
the type of the object or reference being initialized and the source
type is the type of the initializer expression. If the initializer is
not a single (possibly parenthesized) expression, the source type is
not defined.
If the initializer is a (non-parenthesized) braced-init-list, the object or reference is list-initialized (8.5.4).
[...]
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, [...]
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. The function selected is called with the initializer
expression as its argument; if the function is a constructor, the call
initializes a temporary of the cv-unqualified version of the
destination type. The temporary is a prvalue. The result of the call
(which is the temporary for the constructor case) is then used to
direct-initialize, according to the rules above, the object that is
the destination of the copy-initialization. In certain cases, an
implementation is permitted to eliminate the copying inherent in this
direct-initialization by constructing the intermediate result directly
into the object being initialized; see 12.2, 12.8.
[...]
§8.5.1 [dcl.init.aggr]/p2:
When an aggregate is initialized by an initializer list, as specified
in 8.5.4, the elements of the initializer list are taken as
initializers for the members of the aggregate, in increasing subscript
or member order. Each member is copy-initialized from the
corresponding initializer-clause. If the initializer-clause is an
expression and a narrowing conversion (8.5.4) is required to convert
the expression, the program is ill-formed. [ Note: If an
initializer-clause is itself an initializer list, the member is
list-initialized, which will result in a recursive application of the
rules in this section if the member is an aggregate. —end note ]
§8.5.4 [dcl.init.list]/p3:
List-initialization of an object or reference of type T is defined as
follows:
If T is an aggregate, aggregate initialization is performed (8.5.1).
[...]
Otherwise, if T is a class type, constructors are considered. The applicable constructors are enumerated and the best one is chosen
through overload resolution (13.3, 13.3.1.7). If a narrowing
conversion (see below) is required to convert any of the arguments,
the program is ill-formed.
[...]
For constexpr C arr[] = {1, 2};, aggregate initialization copy-initializes each element from the corresponding initializer-clause, i.e., 1 and 2. As described in §8.5 [dcl.init]/p17, this constructs a temporary C and then direct-initializes the array element from the temporary, which requires an accessible copy or move constructor. (The copy/move can be elided, but the constructor must still be available.)
For constexpr C arr[] = {{1}, {2}};, the elements are copy-list-initialized instead, which does not construct temporaries (note the absence of any mention of a temporary being constructed in §8.5.4 [dcl.init.list]/p3).

Any difference between copy-list-initialization and traditional copy-initialization?

Except for supporting multiple arguments, disallowing narrowing conversion, matching constructor taking std::initializer_list argument, what else is different for copy-list-initialization against traditional copy-initialization?
To be specific, assume there are two user-defined types, A and B:
class A {...};
class B {...};
B b;
A a1 = {b};
A a2 = b;
What kind of definition of A and B will make a difference on those two forms of initialization? e.g. Is there a certain definition of A and B that will make one of the initialization legal but the other illegal, or both legal but with different semantics, or both illegal with different causes?
(Assume A doesn't have a constructor taking std::initializer_list argument.)
EDIT: Adding a link to a somewhat related question of mine: What is the supposed behavior of copy-list-initialization in the case of an initializer with a conversion operator?
Copy-initialization always considers availability of copy constructors, while copy-list-initialization doesn't.
class B {};
struct A
{
A(B const&) {}
A(A const&) = delete;
};
B b;
A a1 = {b}; // this compiles
A a2 = b; // this doesn't because of deleted copy-ctor
This is because copy-list-initialization is identical to direct-list-initialization except in one situation - had A(B const&) been explicit, the former would've failed, while the latter will work.
class B {};
struct A
{
explicit A(B const&) {}
};
int main()
{
B b;
A a1{b}; // compiles
A a2 = {b}; // doesn't compile because ctor is explicit
}
Probably, the behaviour of the new copy-list-initialization was defined to be "good" and consistent, but the "weird" behaviour of old copy-initialization couldn't be changed because of backward compatibility.
As you can see the rules for list-initialization in this clause are identical for direct and copy forms.
The difference related to explicit is described only in the chapter on overload resolution. But for traditional initialization direct and copy forms are not identical.
The traditional and brace initializations are defined separately, so there's always a potential for some (probably unintended) subtle differences.
The differences I can see from the excerpts of the standard:
1. Already mentioned differences
narrowing conversions are disallowed
multiple arguments are possible
braced syntax prefers initializer-list constructors if they present:
struct A
{
A(int i_) : i (i_) {}
A(std::initializer_list<int> il) : i (*il.begin() + 1) {}
int i;
}
A a1 = 5; // a1.i == 5
A a2 = {5}; // a2.i = 6
2. Different behaviour for aggregates
For aggregates you can't use braced copy-constructor, but can use traditional one.
struct Aggr
{
int i;
};
Aggr aggr;
Aggr aggr1 = aggr; // OK
Aggr aggr2 = {aggr}; // ill-formed
3. Different behaviour for reference initialization in presence of conversion operator
Brace initialization can't use operators of conversion to reference type
struct S
{
operator int&() { return some_global_int;}
};
int& iref1 = s; // OK
int& iref2 = {s}; // ill-formed
4. Some subtle differences in initialization of object of class type by object of other type
These difference are marked by [*] in the excerpts of the Standard at the end of this answer.
Old initialization uses notion of user-defined conversion sequences (and, particularly, requires availability of copy constructor, as was mentioned)
Brace initialization just performs overload resolution among applicable constructors, i.e. brace initialization can't use operators of conversion to class type
These differences are responsible for some not very obvious (for me) cases like
struct Intermediate {};
struct S
{
operator Intermediate() { return {}; }
operator int() { return 10; }
};
struct S1
{
S1(Intermediate) {}
};
S s;
Intermediate im1 = s; // OK
Intermediate im2 = {s}; // ill-formed
S1 s11 = s; // ill-formed
S1 s12 = {s}; // OK
// note: but brace initialization can use operator of conversion to int
int i1 = s; // OK
int i2 = {s}; // OK
5. Difference in overload resolution
Different treatment of explicit constructors
See 13.3.1.7 Initialization by list-initialization
In copy-list-initialization, if an explicit constructor is chosen, the
initialization is ill-formed. [ Note: This differs from other
situations (13.3.1.3, 13.3.1.4), where only converting constructors
are considered for copy initialization. This restriction only applies
if this initialization is part of the final result of overload
resolution. — end note ]
If you can see more differences or somehow correct my answer (including grammar mistakes), please do.
Here are the relevant (but long) excerpts from the current draft of the C++ standard (I haven't found a way to hide them under spoiler):
All of them are located in the chapter 8.5 Initializers
8.5 Initializers
If the initializer is a (non-parenthesized) braced-init-list, the
object or reference is list-initialized (8.5.4).
If the destination type is a reference type, see 8.5.3.
If the destination type is an array of characters, an array of char16_t, an
array of char32_t, or an array of wchar_t, and the initializer is a
string literal, see 8.5.2.
If the initializer is (), the object is
value-initialized.
Otherwise, if the destination type is an array,
the program is ill-formed.
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. 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.
[*] 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. The function
selected is called with the initializer expression as its argument; if
the function is a constructor, the call initializes a temporary of the
cv-unqualified version of the destination type. The temporary is a
prvalue. The result of the call (which is the temporary for the
constructor case) is then used to direct-initialize, according to the
rules above, the object that is the destination of the
copy-initialization. In certain cases, an implementation is permitted
to eliminate the copying inherent in this direct-initialization by
constructing the intermediate result directly into the object being
initialized; see 12.2, 12.8.
Otherwise, if the source type is a
(possibly cv-qualified) class type, conversion functions are
considered. The applicable conversion functions are enumerated
(13.3.1.5), and the best one is chosen through overload resolution
(13.3). The user-defined conversion so selected is called to convert
the initializer expression into the object being initialized. If the
conversion cannot be done or is ambiguous, the initialization is
ill-formed.
Otherwise, the initial value of the object being
initialized is the (possibly converted) value of the initializer
expression. Standard conversions (Clause 4) will be used, if
necessary, to convert the initializer expression to the cv-unqualified
version of the destination type; no user-defined conversions are
considered. If the conversion cannot be done, the initialization is
ill-formed.
8.5.3 References ...
8.5.4 List-initialization
List-initialization of an object or reference of type T is defined as
follows:
If T is an aggregate, aggregate initialization is
performed (8.5.1).
Otherwise, if the initializer list has no
elements and T is a class type with a default constructor, the object
is value-initialized.
Otherwise, if T is a specialization of
std::initializer_list<E>, a prvalue initializer_list object is
constructed as described below and used to initialize the object
according to the rules for initialization of an object from a class of
the same type (8.5).
[*] Otherwise, if T is a class type,
constructors are considered. The applicable constructors are
enumerated and the best one is chosen through overload resolution
(13.3, 13.3.1.7). If a narrowing conversion (see below) is required to
convert any of the arguments, the program is ill-formed.
Otherwise, if the initializer list has a single element of type E and
either T is not a reference type or its referenced type is
reference-related to E, the object or reference is initialized from
that element; if a narrowing conversion (see below) is required to
convert the element to T, the program is ill-formed.
Otherwise, if
T is a reference type, a prvalue temporary of the type referenced by T
is copy-list-initialized or direct-list-initialized, depending on the
kind of initialization for the reference, and the reference is bound
to that temporary. [ 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 ]
Otherwise, if the initializer list
has no elements, the object is value-initialized.
Otherwise, the program is ill-formed.

Purpose of Explicit Default Constructors

I recently noticed a class in C++0x that calls for an explicit default constructor. However, I'm failing to come up with a scenario in which a default constructor can be called implicitly. It seems like a rather pointless specifier. I thought maybe it would disallow Class c; in favor of Class c = Class(); but that does not appear to be the case.
Some relevant quotes from the C++0x FCD, since it is easier for me to navigate [similar text exists in C++03, if not in the same places]
12.3.1.3 [class.conv.ctor]
A default constructor may be an explicit constructor; such a constructor will be used to perform default-initialization or value initialization (8.5).
It goes on to provide an example of an explicit default constructor, but it simply mimics the example I provided above.
8.5.6 [decl.init]
To default-initialize an object of type T means:
— if T is a (possibly cv-qualified) class type (Clause 9), the default constructor for T is called (and the initialization is ill-formed if T has no accessible default constructor);
8.5.7 [decl.init]
To value-initialize an object of type T means:
— if T is a (possibly cv-qualified) class type (Clause 9) with a user-provided constructor (12.1), then the default constructor for T is called (and the initialization is ill-formed if T has no accessible default constructor);
In both cases, the standard calls for the default constructor to be called. But that is what would happen if the default constructor were non-explicit. For completeness sake:
8.5.11 [decl.init]
If no initializer is specified for an object, the object is default-initialized;
From what I can tell, this just leaves conversion from no data. Which doesn't make sense. The best I can come up with would be the following:
void function(Class c);
int main() {
function(); //implicitly convert from no parameter to a single parameter
}
But obviously that isn't the way C++ handles default arguments. What else is there that would make explicit Class(); behave differently from Class();?
The specific example that generated this question was std::function [20.8.14.2 func.wrap.func]. It requires several converting constructors, none of which are marked explicit, but the default constructor is.
This declares an explicit default constructor:
struct A {
explicit A(int a1 = 0);
};
A a = 0; /* not allowed */
A b; /* allowed */
A c(0); /* allowed */
In case there is no parameter, like in the following example, the explicit is redundant.
struct A {
/* explicit is redundant. */
explicit A();
};
In some C++0x draft (I believe it was n3035), it made a difference in the following way:
A a = {}; /* error! */
A b{}; /* alright */
void function(A a);
void f() { function({}); /* error! */ }
But in the FCD, they changed this (though, I suspect that they didn't have this particular reason in mind) in that all three cases value-initialize the respective object. Value-initialization doesn't do the overload-resolution dance and thus won't fail on explicit constructors.
Unless explicitly stated otherwise, all standard references below refers to N4659: March 2017 post-Kona working draft/C++17 DIS.
(This answer focus specifically on explicit default constructors which have no parameters)
Case #1 [C++11 through C++20]: Empty {} copy-list-initialization for non-aggregates prohibits use of explicit default constructors
As governed by [over.match.list]/1 [emphasis mine]:
When objects of non-aggregate class type T are list-initialized such
that [dcl.init.list] specifies that overload resolution is performed
according to the rules in this section, overload resolution selects
the constructor in two phases:
(1.1) Initially, the candidate functions are the initializer-list constructors ([dcl.init.list]) of the class T and the argument list
consists of the initializer list as a single argument.
(1.2) If no viable initializer-list constructor is found, overload resolution is performed again, where the candidate functions are all
the constructors of the class T and the argument list consists of
the elements of the initializer list.
If the initializer list has no elements and T has a default
constructor, the first phase is omitted. In
copy-list-initialization, if an explicit constructor is chosen, the
initialization is ill-formed. [ Note: This differs from other
situations ([over.match.ctor], [over.match.copy]), where only
converting constructors are considered for copy-initialization. This
restriction only applies if this initialization is part of the final
result of overload resolution.  — end note ]
copy-list-initialization with an empty braced-init-list {} for non-aggregates prohibits use of explicit default constructors; e.g.:
struct Foo {
virtual void notAnAggregate() const {};
explicit Foo() {}
};
void foo(Foo) {}
int main() {
Foo f1{}; // OK: direct-list-initialization
// Error: converting to 'Foo' from initializer
// list would use explicit constructor 'Foo::Foo()'
Foo f2 = {};
foo({});
}
Albeit the standard quote above refers to C++17, this likewise applies for C++11, C++14 and C++20.
Case #2 [C++17 only]: A class type with a user-declared constructor that is marked as explicit is not an aggregate
[dcl.init.aggr]/1 added was updated some between C++14 and C++17, mainly by allowing an aggregate to derive publicly from a base class, with some restrictions, but also prohibiting explicit constructors for aggregates [emphasis mine]:
An aggregate is an array or a class with
(1.1) no user-provided, explicit, or inherited constructors ([class.ctor]),
(1.2) no private or protected non-static data members (Clause [class.access]),
(1.3) no virtual functions, and
(1.4) no virtual, private, or protected base classes ([class.mi]).
As of P1008R1 (Prohibit aggregates with user-declared constructors), which has been implemented for C++20, we may no longer ever declare constructors for aggregates. In C++17 alone, however, we had the peculiar rule that whether a user-declared (but not user-provided) constructor was marked explicit decided whether the class type was an aggregate or not. E.g. the class types
struct Foo {
Foo() = default;
};
struct Bar {
explicit Bar() = default;
};
were aggregates/not aggregates in C++11 through C++20 as follows:
C++11: Foo & Bar are both aggregates
C++14: Foo & Bar are both aggregates
C++17: Only Foo is an aggregate (Bar has an explicit constructor)
C++20: None of Foo or Bar are aggregates (both has user-declared constructors)

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 ]