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.
/Zasupport does not get more attention from Microsoft. The flag removes so many ISO-standard violations, but it's incredibly to hard to use in real code because<windows.h>is not compatible with it, and it has also caused trouble with one or two standard headers in the past./Zais unusable in practice, the latest MSVC 2017 RC using the new/permissive-option correctly rejects the code (with the same error message). cc @ChristianHackl