Неоднозначный вызов конструктора с инициализацией списка

struct A {
    A(int) {}
};

struct B {
    B(A) {}
};

int main() {
    B b({0});
}

Конструкция b дает следующие ошибки:

In function 'int main()':
24:9: error: call of overloaded 'B(<brace-enclosed initializer list>)' is ambiguous
24:9: note: candidates are:
11:2: note: B::B(A)
10:8: note: constexpr B::B(const B&)
10:8: note: constexpr B::B(B&&)

Я ожидал, что B::B(A) будет вызван, почему это двусмысленно в этом случае?

Ответ 1

Учитывая класс, A с определяемым пользователем конструктором:

struct A
{
    A(int) {}
};

и еще один, B, принимающий A как параметр конструктора:

struct B
{
    B(A) {}
};

то для выполнения инициализации, как показано ниже:

B b({0});

компилятор должен учитывать следующие кандидаты:

B(A);         // #1
B(const B&);  // #2
B(B&&);       // #3

пытается найти неявную последовательность преобразований от {0} к каждому из параметров.

Обратите внимание, что B b({0}) не list-initialize B - инициализация списка (copy-) применяется к самому параметру конструктора.

Поскольку аргумент представляет собой список инициализаторов, неявная последовательность преобразования, необходимая для сопоставления аргумента параметру, определяется в терминах последовательности инициализации списка [over.ics.list]/р1:

Когда аргумент представляет собой список инициализаторов ([dcl.init.list]), это не выражение и специальные правила для преобразования его в тип параметра.

Он гласит:

[...], если параметр является неагрегатным классом X и разрешением перегрузки на 13.3.1.7 выбирает один лучший конструктор X для выполнения инициализации объекта типа X из списка инициализатора аргументов, неявная последовательность преобразования представляет собой пользовательскую последовательность преобразования со вторым стандартным преобразованием последовательность преобразования идентичности. Если несколько конструкторов жизнеспособны, но ни один не лучше других, неявная последовательность преобразований - это неоднозначная последовательность преобразований. Разрешены определенные пользователем преобразования. для преобразования элементов списка инициализаторов в типы параметров конструктора, за исключением случаев, указанных в 13.3.3.1.

Для того, чтобы # 1 был жизнеспособным, должен быть действителен следующий вызов:

A a = {0};

что является правильным из-за [over.match.list]/p1:

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

i.e, class A имеет конструктор, который принимает аргумент int.

Чтобы №2 был действительным кандидатом, должен быть действителен следующий вызов:

const B& b = {0};

который согласно [over.ics.ref]/p2:

Когда параметр ссылочного типа не привязан непосредственно к выражению аргумента, последовательность преобразования является той, которая требуется для преобразования выражения аргумента в ссылочный тип в соответствии с [over.best.ics]. Понятно, что эта последовательность преобразования соответствует инициализации копирования временного ссылочного типа с выражением аргумента. Любая разница в квалификационной квалификации верхнего уровня включается самой инициализацией и не представляет собой преобразования.

переводит на:

B b = {0};

Еще раз, после [over.ics.list]/p6:

Пользовательские преобразования разрешены для преобразования элементов списка инициализаторов в типы параметров конструктора [...]

компилятору разрешено использовать пользовательское преобразование:

A(int);

чтобы преобразовать аргумент 0 в B параметр конструктора A.

Для кандидата № 3 те же рассуждения применимы, что и в # 2. В конце концов, компилятор не может выбирать между вышеупомянутыми неявными последовательностями преобразования {citation} и сообщает о двусмысленности.

Ответ 2

B b({0}) может привести к вызову любого из следующих действий:

  • B::B(A)

  • Конструктор копирования B: создание временного объекта B из {0} и затем скопируйте его на B.

Отсюда двусмысленность.

Он может быть разрешен, если вы вызываете B b{0}, который напрямую использует определенный конструктор без участия конструктора экземпляра.

EDIT:

Что касается точки 2:

B имеет конструктор, который принимает A. Теперь A можно построить с помощью int. Кроме того, int может быть создан через список инициализации. Вот почему это действительный случай. Если конструктор A был explicit, автоматическое кастинг от {0} до int потерпел бы неудачу, что не привело бы к двусмысленности.

Ответ 3

Код компилируется с GCC8.

Это не должно быть двусмысленным вызовом. Для вызова экземпляра copy/move B, тогда для B b({0}); требуются следующие шаги:

  • построить A из 0 на A::A(int)
  • построить B из A, построенный на шаге 1, на B::B(A)
  • построить B из B, построенный на этапе2, с помощью конструктора copy/move B.

Это означает, что требуются две пользовательские преобразования (шаги №1 и №2), но это не допускается в одной неявной последовательности конверсии.