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

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), почему в данном случае это неоднозначно?


person ashen    schedule 28.04.2017    source источник


Ответы (3)


Дан класс 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}) не выполняет инициализацию списка b - инициализация списка (копирования) применяется к самому параметру конструктора.

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

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

Он гласит:

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

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

A a = {0};

что верно из-за [over.match.list] / p1:

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

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

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

const B& b = {0};

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

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

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

B b = {0};

Еще раз, следуя [over.ics.list] / p6 :

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

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

A(int);

для преобразования аргумента 0 в параметр конструктора B A.

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

person Piotr Skotnicki    schedule 01.05.2017
comment
Я не совсем уверен, но, вероятно, это выглядит так - person Piotr Skotnicki; 01.05.2017
comment
мне кажется, лучше объяснили. - person ashen; 02.05.2017

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

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

  1. построить A из 0 по A::A(int)
  2. построить B из A, созданного на шаге 1 с помощью B::B(A)
  3. построить b из B, созданного на шаге 2 конструктором копирования / перемещения B.

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

person songyuanyao    schedule 28.04.2017
comment
@ashen Какой компилятор (и версию) вы используете? - person songyuanyao; 28.04.2017
comment
Это правильный ответ. Он не работает на компиляторах icc и vc ++ v141 (2017). - person unexpectedvalue; 28.04.2017
comment
Что случилось с GCC 7? Его съело то же самое, что съело Windows 9 от Microsoft? - person Cody Gray; 28.04.2017
comment
@CodyGray ответвился от ствола (8.0) - person Piotr Skotnicki; 28.04.2017
comment
@songyuanyao gcc 4.9.2. Я пробовал вашу ссылку, она компилируется только с gcc 8 под c ++ 1z. Если вы попробуете более ранние версии, он все равно не компилируется ... - person ashen; 28.04.2017
comment
@ashen Я тоже это заметил. Я попытался выяснить, не изменилось ли что-то по сравнению с C ++ 17, но безуспешно. Насколько я понимаю, код должен нормально работать с C ++ 11. - person songyuanyao; 28.04.2017
comment
[over.match.funcs] / p6 означает, что общая из двух заданных пользователем преобразований разрешены в последовательности инициализации списка - person Piotr Skotnicki; 02.05.2017
comment
@PiotrSkotnicki Я думаю, об этом должно быть несколько явных заявлений. Но я не могу найти ничего в dcl.init.list .. - person songyuanyao; 02.05.2017
comment
@songyuanyao, если это не список инициализации в другом списке инициализации, для элементов разрешено определяемое пользователем преобразование; проверьте примеры из [over.ics.list] / p6 , h({"foo"}); в частности - person Piotr Skotnicki; 02.05.2017

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

  1. B::B(A)

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

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

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

РЕДАКТИРОВАТЬ:

Относительно того, как действует пункт 2:

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

person CinCout    schedule 28.04.2017
comment
Это то, что говорит компилятор, и не объясняет, почему это происходит. - person Piotr Skotnicki; 28.04.2017
comment
@PiotrSkotnicki Не уверен, что вы имеете в виду под почему это происходит. Неоднозначность возникает, когда при разрешении перегрузки обнаруживается более одной возможности, что явно имеет место в данном случае. - person CinCout; 28.04.2017
comment
О, так что двусмысленность возникает из-за неоднозначного вызова? Я бы никогда не догадалась - person Piotr Skotnicki; 28.04.2017
comment
Неоднозначность возникает, когда разрешение перегрузки находит более одной возможности, компилятор уже сказал, какой вызов является неоднозначным, и перечислил кандидатов, вопрос в том, почему вызов неоднозначен в соответствии с текущими правилами и должен ли он быть неоднозначным - person Piotr Skotnicki; 28.04.2017
comment
@CinCout в большинстве случаев, когда разрешение перегрузки находит более одной возможности, есть одна предпочтительная возможность, которая побеждает в процессе ранжирования. - person M.M; 28.04.2017
comment
Я не использую вариант 2. Почему B создается из списка инициализаторов с 0? Я легко мог что-то упустить, но, по крайней мере, это не очевидно и заслуживает объяснения. B не имеет конструкторов, принимающих целое число или список инициализации, и даже не имеет конструктора по умолчанию! - person Nir Friedman; 28.04.2017
comment
Добавлено объяснение. - person CinCout; 28.04.2017
comment
Если бы конструктор A был явным, автоматическое преобразование из {0} в int не удалось бы, что не привело бы к двусмысленности. вы пробовали это? - person Piotr Skotnicki; 28.04.2017
comment
@PiotrSkotnicki: Это больше не двусмысленно, нет подходящих кандидатов ;-) - person Jarod42; 28.04.2017