Поскольку существует два способа определения преобразования в C ++, как они взаимодействуют, когда есть две возможности для одного и того же преобразования?

Я просто ищу разъяснения о том, как работает C ++, на самом деле речь не идет о решении конкретной проблемы в моем коде.

В C ++ вы можете сказать, что тип A должен неявно преобразовываться в тип B двумя различными способами.

Если вы являетесь автором A, вы можете добавить что-то вроде этого в A:

operator B() {
// code
}

Если вы являетесь автором B, вы можете добавить что-то вроде этого в B:

B(const A &a) {
// code
}

Любой из них, если я правильно понимаю, позволит A неявно преобразовать в B. Так что, если оба определены, какой из них используется? Это даже разрешено?

ПРИМЕЧАНИЕ: я понимаю, что вы, вероятно, никогда не должны быть в ситуации, когда вы делаете это. Вы должны либо сделать конструктор явным, либо, что более вероятно, иметь только один из двух. Мне просто интересно, что говорит спецификация C ++, и я не знаю, как это найти.

7

Решение

К сожалению, ответ на этот вопрос, вероятно, более сложный, чем вы искали. Это правда, что компилятор будет отклонять неоднозначные преобразования, как указывают гонки легкости на орбите, но являются ли преобразования неоднозначными? Давайте рассмотрим несколько случаев. Все ссылки на стандарт C ++ 11.

Это не решает ваш вопрос напрямую, потому что вы спрашивали о неявном преобразовании, но поскольку Гонки Легкости на Орбите привели пример явного преобразования, я все равно расскажу об этом.

Явное преобразование выполняется из A в B когда:

  • вы используете синтаксис (B)a, где a имеет тип A, который в этом случае будет эквивалентен static_cast<B>(a) (Стандарт C ++ 11, §5.4 / 4).
  • вы используете статическое приведение, которое в этом случае создаст временный объект, который инициализируется так же, как объявление B t(a); инициализирует t; (§5.2.9 / 4)
  • вы используете синтаксис B(a), что эквивалентно (B)a и, следовательно, также делает то же самое, что и инициализация в объявлении B t(a); (§5.2.3 / 1)

Поэтому в каждом случае прямая инициализация выполняется с типом значения B используя значение типа A в качестве аргумента. §8.5 / 16 указывает, что рассматриваются только конструкторы, так B::B(const A&) будет называться. (Для более подробной информации, смотрите мой ответ здесь: https://stackoverflow.com/a/22444974/481267)

В копии-инициализации

B b = a;

Значение a типа A сначала преобразуется во временный тип B используя пользовательскую последовательность преобразования, которая является неявной последовательностью преобразования. Затем этот временный используется для прямой инициализации b,

Поскольку это инициализация копирования типа класса объектом другого типа, и то и другое конвертирующий конструктор B::B(const A&) и функция преобразования A::operator B() являются кандидатами на преобразование (§13.3.1.4). Последний называется потому, что это выигрывает разрешение перегрузки. Обратите внимание, что если B::B имел аргумент A& скорее, чем const A&перегрузка будет неоднозначной, и программа не будет компилироваться. Подробности и ссылки на стандарт см. В этом ответе: https://stackoverflow.com/a/1384044/481267

Копия-список-инициализация

B b = {a};

учитывает только конструкторы B (§8.5.4 / 3), а не функции преобразования A, так B::B(const A&) будет вызван, как при явном преобразовании.

Если у нас есть

void f(B b);
A a;
f(a);

тогда компилятор должен выбрать лучшую неявную последовательность преобразования для преобразования a печатать B чтобы передать его f, Для этой цели рассматриваются определяемые пользователем последовательности преобразования, которые состоят из стандартного преобразования, за которым следует пользовательское преобразование, за которым следует другое стандартное преобразование (§13.3.3.1.2 / 1). Пользовательское преобразование может происходить через конструктор преобразования. B::B(const A&) или через функцию преобразования A::operator B(),

Вот где это становится сложным. В стандарте есть некоторая запутанная формулировка:

Поскольку неявная последовательность преобразования является инициализацией, специальные правила для инициализации
пользовательским преобразованием применяется при выборе лучшего пользовательского преобразования для пользовательского преобразования
последовательность (см. 13.3.3 и 13.3.3.1).

(§13.3.3.1.2 / 2)

Короче говоря, это означает, что пользовательское преобразование в пользовательской последовательности преобразования из A в B сам по себе подвержен разрешению перегрузки; A::operator B() побеждает B::B(const A&) потому что первый имеет меньше cv-квалификации (как в случае инициализации копирования), и двусмысленность может возникнуть, если бы мы имели B::B(A&) скорее, чем B::B(const A&), Обратите внимание, что это не может привести к бесконечной рекурсии разрешения перегрузки, поскольку пользовательские преобразования не допускаются для преобразования аргумента в тип параметра пользовательского преобразования.

В

B foo() {
return A();
}

выражение A() неявно преобразуется в тип B (§6.6.3 / 2), поэтому применяются те же правила, что и при неявном преобразовании аргументов функции; A::operator B() будет вызван, и перегрузка будет неоднозначной, если бы мы имели B::B(A&), Однако, если бы это было вместо

return {A()};

тогда вместо этого будет инициализация списка копирования (снова §6.6.3 / 2); B::B(const A&) будет называться.

Замечания: определяемые пользователем преобразования не выполняются при обработке исключений; catch(B) блок не справится throw A();,

1

Другие решения

[C++11: 12.3/2]: Определяемые пользователем преобразования применяются только там, где они однозначны. [..]

12.3. Перечислим два вида, которые вы определили.

1