12

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
11
  • 1
    "Is that a conversion compilers are allowed to do?" No. The rule is clear, MSVC is wrong. Commented Jan 11, 2017 at 15:07
  • 1
    @steveire: Thanks! It's such a pity that /Za support 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. Commented Jan 11, 2017 at 15:50
  • 2
    Just a note to say that, while /Za is unusable in practice, the latest MSVC 2017 RC using the new /permissive- option correctly rejects the code (with the same error message). cc @ChristianHackl Commented Jan 16, 2017 at 23:09
  • 1
    Regarding the question(s), I think a proper answer would essentially duplicate a lot of existing, good reference material on the difference between copy-initialization and direct-initialization, overload resolution, and implicit conversion sequences. I'd say you'd be better off looking those up and working through them. It will take some time; the topic is complex. Commented Jan 16, 2017 at 23:14
  • 1
    @bogdan: Wow, that sounds great. blogs.msdn.microsoft.com/vcblog/2016/11/16/permissive-switch says "Visual Studio 2017 RC will feature a mode much closer to ISO C++ standards conformance than any time in its history" and "the /permissive- switch [...] will one day become the default mode for the Visual C++ compiler". Good news for C++ :) Commented Jan 17, 2017 at 7:37

3 Answers 3

11
+150

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.

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

5 Comments

The result of MyString --> const char* is const char*. There must be another change, namely const char* --> YourString, right? Are you saying that because that is done with a constructor it is not a conversion ? If that's it, please add that to the answer. Also, it seems that part of the key here is that the case that does not work does not call operator=(). Is that right? If so, please make some point about that in the answer.
@steveire I don't understand your first two questions. As for your third, yes, as noted in the answer, it's copy-initialization. I walked through what that actually means - and it does not mean operator=
you said that there is one single conversion: MyString --> const char* - however, I am not converting to const char* I am converting to YourString. So, what you said can only be half the answer. That's what "my first two questions" are about. Will you read them again and see if you understand them now?
@steveire In direct-initialization, you are converting MyString to const char* to invoke the YourString(const char*) constructor. That's one single conversion.
Then the answer to my second question is simply 'yes' :).
2

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.

2 Comments

Thanks for your answer. It seems to be that in case 2, we also 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 *)... Is the real difference that after doing both of those, case 3 requires a copy constructor to be called additionally?
Image
@steveire In case 2, the single argument constructor does not have to be deduced, it's explicitly provided.
0

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 =. ...

3 Comments

I didn't claim they were equivalent either, but you word your answer as if I did. I guess you are referring to my question about why MSVC is doing what it does. I'm looking for an explanation of why an implicit conversion+implicit constructor is acceptable. Thanks for your answer, but I'm looking for something meatier.
"The first approach uses the constructor YourString() {}" -- I removed this constructor (and the first case of the main code) and MSVC still accepted it by default without warning. How can what you said be true if there is no default constructor?
I am not sure if I understand well what you mean. When you remove the first case of the main code, you remove the part that I referred to as the first approach.

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.