Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

Tags:

c++

visual-c++

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
like image 788
steveire Avatar asked Jan 11 '17 15:01

steveire


People also ask

How many types of user-defined conversions are there?

There are two types of user-defined conversions: Conversion constructors and conversion functions. The compiler can use only one user-defined conversion (either a conversion constructor or a conversion function) when implicitly converting a single value.

Which of the following is the user-defined type conversion?

User-defined conversions perform conversions between user-defined types, or between user-defined types and built-in types. You can implement them as Conversion constructors or as Conversion functions.

What is the use of type conversion write the standard conversion rules in C++?

Type Conversion in C++ Done by the compiler on its own, without any external trigger from the user. Generally takes place when in an expression more than one data type is present. In such condition type conversion (type promotion) takes place to avoid lose of data.

Which operator can be used to do type conversions?

operator int(); ... }; The operator overloading defines a type conversion operator that can be used to produce an int type from a Counter object. This operator will be used whenever an implicit or explict conversion of a Counter object to an int is required. Notice that constructors also play a role in type conversion.


2 Answers

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.

like image 185
Barry Avatar answered Sep 25 '22 20:09

Barry


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.

like image 29
François Andrieux Avatar answered Sep 22 '22 20:09

François Andrieux