Неоднозначный оператор присваивания

У меня есть два класса, один из которых, скажем, представляет строку, а другой может быть преобразован в строку:

class A {
public:
  A() {}
  A(const A&) {}
  A(const char*) {}

  A& operator=(const A&) { return *this; }
  A& operator=(const char*) { return *this; }

  char* c;
};
class B {
public:
  operator const A&() const {
    return a;
  }
  operator const char*() const {
    return a.c;
  }

  A a;
};

Теперь, если я делаю

B x;
A y = x;

Он запускает конструктор копирования, который компилируется отлично. Но если я делаю

A y;
y = x;

Он жалуется на двусмысленное присваивание и не может выбирать между =(A&) и =(char*). Почему разница?

Ответ 1

Существует разница между инициализацией и присваиванием.

При инициализации, то есть:

A y = x;

Фактический вызов зависит от типа x. Если это тот же тип y, то он будет выглядеть так:

A y(x);

Если нет, как в вашем примере, это будет выглядеть так:

A y(static_cast<const A&>(x));

И это прекрасно компилируется, потому что больше нет двусмысленности.

В присваивании нет такого особого случая, поэтому автоматическое разрешение двусмысленности отсутствует.

Стоит отметить, что:

A y(x);

также неоднозначен в вашем коде.

Ответ 2

Существует §13.3.1.4/(1.2), только относящийся к (copy-) инициализации объектов типа класса, который определяет, как будут найдены функции преобразования кандидатов для вашего первого случая:

В условиях, указанных в 8.5, как часть копирование-инициализация объекта типа класса, определяемая пользователем преобразование может быть вызвано для преобразования выражения инициализатора в тип инициализированного объекта. Разрешение перегрузки используется для выберите пользовательское преобразование для вызова. […] При условии, что "cv1 T" - это тип инициализированного объекта, с T классом тип, выбранные функции выбираются следующим образом:

  • Конструкторы преобразования (12.3.1) of T являются кандидатами функции.

  • Когда тип выражения инициализатора является типом класса "cv S", неявные функции преобразования S и его базы классы. При инициализации временной привязки к первый параметр конструктора, где параметр имеет тип "ссылка на возможно cv-qual T", и конструктор называется с одним аргументом в контексте прямой инициализации объект типа "cv2 T", явные функции преобразования также считается. Те, которые не скрыты внутри S и выдают тип чья cv-неквалифицированная версия имеет тот же тип, что и T, или является производным класс является функциями-кандидатами. [...] Функции преобразования, возвращающие "ссылку на X" return lvalues ​​или xvalues, в зависимости от типа ссылки, типа X и, следовательно, считается, что для этого процесса выбора кандидатских функций требуется X.

т.е. operator const char*, хотя и рассматривается, не входит в набор кандидатов, поскольку const char* явно не похож на A в любом отношении. Однако во втором фрагменте operator= вызывается как обычная функция-член, поэтому это ограничение больше не применяется; Когда обе функции преобразования находятся в наборе кандидатов, разрешение перегрузки явно приведет к двусмысленности.

Обратите внимание, что для прямой инициализации указанное правило также не применяется.

B x;
A y(x);

Неправильно сформирован.

Более общая форма этого результата состоит в том, что в одной последовательности преобразования никогда не может быть двух определяемых пользователем преобразований при разрешении перегрузки. Рассмотрим §13.3.3.1/4:

Однако, если цель

  • первый параметр конструктора или [...]

и конструктор [...] является кандидатом по

  • 13.3.1.3, когда аргумент является временным на втором этапе инициализации экземпляра класса или
  • 13.3.1.4, 13.3.1.5 или 13.3.1.6 (во всех случаях),

пользовательские последовательности преобразований не рассматриваются. [Примечание: эти правила предотвращают применение более одного пользовательского преобразования во время разрешения перегрузки, тем самым избегая бесконечной рекурсии. - конец примечание]