Неявные преобразования C++ с инициализаторами фигурных скобок

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

Учитывать:

#include <string>

using ::std::string;

struct C {
  C() {}
};

struct A {
  A(const string& s) {}  // Make std::string convertible to A.
  operator C() const { return C(); }  // Makes A convertible to C.
};

struct B {
  B() {}
  B(const A& a) {}  // Makes A convertible to B.
};

int main() {
  B b;
  C c;

  // This works.
  // Conversion chain (all thru ctors): char* -> string -> A -> B
  b = {{"char *"}};

  // These two attempts to make the final conversion through A's
  // conversion method yield compiler errors.
  c = {{"char *"}};
  c = {{{"char *"}}};
  // On the other hand, this does work (not surprisingly).
  c = A{"char *"};
}

Теперь я могу неправильно интерпретировать то, что делает компилятор, но (на основе вышеизложенного и дополнительных экспериментов) мне кажется, что он не рассматривает преобразования методом преобразования. Однако, прочитав разделы 4 и 13.3.3.1 стандарта, я не смог понять, почему это так. Каково объяснение?

Обновить

Вот еще одно интересное явление, которое я хотел бы объяснить. Если я добавлю

struct D {
  void operator<<(const B& b) {}
};

и в main:

  D d;
  d << {{ "char *" }};

Я получаю сообщение об ошибке, но если вместо этого я пишу d.operator<<({{ "char *" }});, все работает нормально.

Обновление 2

Похоже, раздел 8.5.4 стандарта может содержать некоторые ответы. Я сообщу о своих выводах.


person Ari    schedule 27.05.2016    source источник
comment
Инициализация использует конструкторы и не будет использовать оператор преобразования промежуточного типа. Два нерабочих примера терпят неудачу, потому что неявное построение A для использования его operator C противоречит этому.   -  person Peter    schedule 27.05.2016
comment
Связанный: заголовок stackoverflow.com/questions/35790664/   -  person    schedule 27.05.2016
comment
Питер, я пытаюсь понять, что такое правила. Если я пишу c = A{... или c = {A{..., он отлично работает через метод преобразования. Почему он решает использовать только ctors, если я отбрасываю A?   -  person Ari    schedule 27.05.2016
comment
Надеюсь, вы понимаете, что иметь что-то подобное в каком-либо коде, который не будет заброшен через месяц, было бы довольно плохой идеей. Всегда лучше знать, что делает компилятор, чем позволять ему сходить с ума.   -  person Francesco Dondi    schedule 27.05.2016
comment
Эм что. Почему вы ожидаете, что C неявно преобразуется в несвязанный класс A?   -  person uh oh somebody needs a pupper    schedule 28.05.2016


Ответы (2)


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

In b = {{"char *"}};

мы на самом деле делаем

b = B{{"char*"}}; // B has constructor with A (and a copy constructor not viable here)

so

b = B{A{"char*"}}; // One implicit conversion const char* -> std::string

в c = {{"const char*"}} мы пытаемся

c = C{{"char *"}}; // but nothing to construct here.
person Jarod42    schedule 27.05.2016
comment
@Ари: подойдет c = C{A{std::string{"char*"}}}. происходит толькоA-›C конверсия. - person Jarod42; 27.05.2016
comment
Но вот в чем вопрос, не так ли? Почему этого не происходит при удалении A? Я думал, вы говорите, что C{{ не работает, потому что C не имеет ctor, который принимает A в качестве аргумента. Ясно, что C{A или просто {A не строит, а преобразует методом A. Почему это не происходит без произнесения A? - person Ari; 27.05.2016
comment
Не существует конструктора C, который принимает A, поэтому {{"char*"}} нельзя вывести для типа A. - person Jarod42; 27.05.2016
comment
@ Jarod42 Ари спрашивает, почему он ограничен ctor, когда доступен оператор преобразования. Ваш ответ просто повторяет то, что он заявляет в своем вопросе. - person kfsone; 27.05.2016

Копаясь в разделе 8.5.4 стандарта и следуя различным перекрестным ссылкам в нем, я думаю, что понимаю, что происходит. Конечно, IANAL, так что я могу ошибаться; это моя лучшая попытка.

Обновление: в предыдущей версии ответа использовалось несколько конверсий. Я обновил его, чтобы отразить мое текущее понимание.

Ключом к разгадке беспорядка является тот факт, что список инициализации в фигурных скобках не является выражением (что также объясняет, почему d << {{"char *"}} не будет компилироваться). Это специальный синтаксис, регулируемый специальными правилами, который разрешен в ряде конкретных контекстов. Из этих контекстов важными для нашего обсуждения являются: правая сторона присваивания, аргумент в вызове функции и аргумент в вызове конструктора.

Так что же происходит, когда компилятор видит b = {{"char *"}}? Это случай правого присваивания. Применимое правило:

список инициализации в фигурных скобках может появиться справа от ... присваивания, определяемого определяемым пользователем оператором присваивания, и в этом случае список инициализаторов передается в качестве аргумента оператору. функция.

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

Таким образом, мы вынуждены передавать аргумент оператору присваивания копирования по умолчанию B::operator=(const B&), где передается аргумент {{"char *"}}. Поскольку список инициализации в фигурных скобках не является выражением, здесь нет проблемы преобразования, а скорее форма инициализации временного типа B, в частности, так называемая инициализация списка.

Если жизнеспособный конструктор списка инициализаторов не найден, снова выполняется разрешение перегрузки, где функциями-кандидатами являются все конструкторы класса T, а список аргументов состоит из элементов списка инициализаторов.

Таким образом, компилятор удаляет внешнюю пару фигурных скобок и выполняет разрешение перегрузки, используя {"char *"} в качестве аргумента. Это удается, совпадая с конструктором B::B(const A&), потому что снова происходит инициализация списка временного типа A, в котором разрешение перегрузки соответствует A::A(const string&) для аргумента "char *", что возможно с помощью одного выделенного определяемого пользователем преобразования, а именно из char* в string .

Теперь, в случае c = {{"char *"}}, процесс аналогичен, но когда мы пытаемся инициализировать временный список типа C с помощью {{"char *"}}, разрешение перегрузки не может найти подходящий конструктор. Дело в том, что по определению инициализация списка работает только через конструктор, чей список параметров может соответствовать содержимому списка.

person Ari    schedule 28.05.2016