Какие правила регулируют использование нескольких пользовательских преобразований между типами?

У меня есть этот код:

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 принимает его без проблем. Clang-CL не принимает его:

$ "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.

Не делает 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;
                           ^

Я понимаю, что разрешено только одно пользовательское преобразование.

Однако, MSVC оправдан при обработке строки

YourString yoursBreaks = mys;

как

YourString yoursBreaks(mys);

и принять его? Это разрешено делать компиляторам преобразования? По каким правилам это разрешено/запрещено? Есть ли подобное правило?

Обновление: при использовании MSVC флаг /Za заставляет код не принимать.

$ "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

Ответ 1

TL;DR; Код плохо сформирован, MSVC ошибочно принимает его. Копирование-инициализация отличается от прямой инициализации. Объяснение непрофессионала заключается в том, что инициализация yoursBreaks будет включать в себя два пользовательских преобразования (MyString --> const char* --> YourString), тогда как прямая инициализация включает одно пользовательское преобразование (MyString --> const char*), и вам разрешено не более одного определяемого пользователем преобразование. Стандартное объяснение, которое применяет это правило, заключается в том, что [over.best.ics] не разрешает определяемые пользователем преобразования в контексте инициализации копирования типа класса из несвязанного типа класса путем преобразования конструктора.


К стандарту! Что делает:

YourString yoursBreaks = mys;

означает? Каждый раз, когда мы объявляем переменную, это какая-то инициализация. В этом случае, согласно [dcl.init],

Инициализация, которая встречается в форме = элемента-элемента или условия (6.4), а также при передаче аргумента, возвращает функцию, исключая исключение (15.1), обрабатывая исключение (15.3 ) и инициализация агрегатного члена (8.6.1), называется копирование-инициализация.

Копирование-инициализация - это что-то вроде формы T var = expr; Несмотря на появление =, это никогда не вызывает operator=. Мы всегда проходим через конструктор или функцию преобразования.

В частности, этот случай:

Если тип назначения является (возможно, cv-квалифицированным) типом класса:
- Если выражение инициализатора является prvalue, а cv-неквалифицированная версия типа источника одинакова класс как класс места назначения, [...]
- В противном случае, если инициализация является прямой инициализацией или если она является инициализацией копирования, cv-неквалифицированная версия типа источника - это тот же класс, что и производный класс класса пункт назначения, [...]
- В противном случае (то есть для остальных случаев инициализации копии) пользовательские последовательности преобразования, которые может конвертировать из типа источника в тип назначения или (когда используется функция преобразования), чтобы их производный класс перечисляется, как описано в 13.3.1.4, и наилучший выбирается посредством (13.3). Если преобразование не может быть выполнено или является неоднозначным, инициализация плохо сформирован.

Мы попадаем в эту последнюю пулю. Пусть перескакивает в 13.3.1.4:

- Конструкторы преобразования (12.3.1) из T являются кандидатными функциями.
- Когда тип выражения инициализатора является типом класса "cv S", рассматриваются неявные функции преобразования S и его базовые классы. При инициализации временной привязки к первому параметру конструктора, где параметр имеет тип "ссылка на возможно cv-квалифицированный T", и конструктор вызывается с единственным аргументом в контексте прямой инициализации объекта типа "cv2 T", также рассматриваются явные функции преобразования. Те, которые не скрыты внутри S, и выдают тип, чья неквалифицированная версия cv - это тот же тип, что и T или является его производным классом, являются функциями-кандидатами. Функции преобразования, возвращающие "ссылка на X", возвращают значения l или значения x, в зависимости от типа ссылаясь на тип X, и поэтому считается, что для этого процесса выбора кандидатских функций он дает X.

Первая маркерная точка дает нам конструкторы преобразования YourString, которые:

YourString(const char* );

Вторая пуля нам ничего не дает. MyString не имеет функции преобразования, которая возвращает YourString или тип класса, полученный из него.


Итак, ладно. У нас есть один конструктор-кандидат. Это жизнеспособно? [over.match] проверяет надежность через:

Тогда лучшая жизнеспособная функция выбирается на основе неявных последовательностей преобразования (13.3.3.1), необходимых для сопоставления каждого аргумента с соответствующим параметром каждой жизнеспособной функции.

и в [over.best.ics]:

Хорошо сформированная неявная последовательность преобразования является одной из следующих форм:
- стандартная последовательность преобразования (13.3.3.1.1),
- пользовательская последовательность преобразования (13.3.3.1.2), или
- последовательность преобразования многоточия (13.3.3.1.3).

Однако, если цель - - первый параметр конструктора или
- неявный объект-параметр пользовательской функции преобразования

а конструктор или определяемая пользователем функция преобразования является кандидатом от
- 13.3.1.3, когда аргумент является временным на втором этапе инициализации копирования класса,
- 13.3.1.4, 13.3.1.5 или 13.3.1.6 (во всех случаях), или
- второй этап 13.3.1.7 [...]
пользовательские последовательности преобразований не учитываются. [Примечание. Эти правила предотвращают более чем одну определяемую пользователем преобразование от применения во время разрешения перегрузки, тем самым избегая бесконечной рекурсии. -end note] [Пример:

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 пример]

Итак, хотя существует последовательность преобразований от MyString до const char*, в этом случае она не рассматривается, поэтому этот конструктор не является жизнеспособным.

Поскольку у нас нет другого конструктора-кандидата, вызов плохо сформирован.


Другая строка:

YourString yoursAlsoWorks(mys);

называется прямой инициализацией. Мы вызываем во вторую маркерную точку из трех блоков [dcl.init], которые я цитировал ранее, которая в целом читает:

Соответствующие конструкторы перечислены (13.3.1.3), и лучший выбирается с помощью разрешения перегрузки (13.3). Выбранный таким образом конструктор вызывается для инициализации объекта с выражением инициализатора или списком выражений в качестве аргумента (ов). Если конструктор не применяется или разрешение перегрузки неоднозначно, инициализация плохо сформирована.

где 13.3.1.3 указывает, что конструкторы перечислены из:

Для прямой инициализации или инициализации по умолчанию, которая не находится в контексте инициализации копирования, все функции-кандидаты являются конструкторами класса инициализируемого объекта.

Эти конструкторы:

YourString(const char* )        // yours
YourString(YourString const& )  // implicit
YourString(YourString&& )       // implicit

Чтобы проверить жизнеспособность последних двух функций, мы повторно выполняем разрешение перегрузки из контекста инициализации копирования (который не соответствует указанному выше). Но для вашего YourString(const char*), это просто, существует жизнеспособная функция преобразования от MyString до const char*, поэтому она используется.

Обратите внимание, что здесь есть одно единственное преобразование: MyString --> const char*. Одно преобразование в порядке.

Ответ 2

Посмотрите на правила для неявных преобразований найденные здесь. Интересный бит:

Неявные преобразования выполняются всякий раз, когда выражение какого-либо типа T1 используется в контексте, который не принимает этот тип, но принимает некоторый другой тип T2; в частности: [...] при инициализации нового объекта типа T2 [...]

и

Пользовательское преобразование состоит из нуля или одного неявного конструктора с одним аргументом или вызова неявной функции преобразования

Случай 1

YourString yoursWorks;
yoursWorks = mys;

В первом случае нам нужен один неявный вызов функции преобразования. YourString::operator= ожидает const char* и задается a MyString. MyString предоставляет неявную функцию преобразования для этого преобразования.

Случай 2

YourString yoursAlsoWorks(mys);

Во втором случае снова нужен один неявный вызов функции преобразования. YourString::YourString ожидает const char* и получает значение MyString. MyString предоставляет неявную функцию преобразования для этого преобразования.

Случай 3

YourString yoursBreaks = mys;

Третий случай отличается тем, что он не является копией присваивания, как он появился. Вопреки второму случаю, yoursBreaks еще не инициализирован. Вы не можете вызвать оператор присваивания operator= объекта, который еще не был создан. На самом деле это назначение путем создания копии. Чтобы назначить mys на yoursBreaks, нам нужно и неявный вызов функции преобразования (для преобразования mys в const char*), а затем неявный конструктор с одним аргументом (для построения YourString из const char *. Неявные преобразования допускают только один или другой.

Ответ 3

Прежде всего

YourString yoursWorks;
yoursWorks = mys;

не эквивалентно

YourString yoursAlsoWorks(mys);

или

YourString yoursBreaks = (const char*) mys;

В первом подходе используется конструктор

YourString() {}

за которым следует оператор преобразования MyString, и YourString оператор присваивания.

Второй подход использует конструктор

YourString(const char* ptr) {(void)ptr;}

и оператор преобразования MyString.

(Это может быть продемонстрировано добавлением инструкций трассировки к конструкторам.)

Затем, когда в последнем заявлении отсутствует (const char *), MSVC предположит, что он должен быть добавлен неявно. Хотя это выглядит разумным подходом, оно конфликтует с описанием в книге Stroustrup Язык программирования С++ 4-е издание:

18.4 Преобразование типа      ...     то есть преобразование выполняется только в непосредственной инициализации, то есть в качестве инициализатора, не использующего a =.     ...