Неоднозначность во время инициализации

Имеются две структуры:

using std::cout;
using namespace std::string_literals;

struct MyVector {
     MyVector(int size) {
         cout << "MyVector(int size)\n"s;
    }
};

struct Gadget {
    Gadget(MyVector v) {
        cout << "Gadget(MyVector v)\n"s;
    }

    Gadget(Gadget&& gd) {
        cout << "Move\n"s;
    }

}; 

int main() {
    Gadget g({10}); // Ошибка!
    return 0;
}

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

  1. Ожидаемый нами вызов конструктора Gadget(MyVector)
  2. Через временный объект и move-конструктор: При указании фигурных скобок, создается Gadget за счет вызова Gadget(MyVector v). Это происходит из-за "copy list initialization". В результате полученный объект передается в качестве аргумента целевому: Gadget g(полученный_объект);, что и может привести к альтернативному пути инициализации через move-конструктор, а следовательно к неоднозначности.

Рассмотрим еще одну структуру:

struct MyStruct {

    MyStruct(int x) {
        cout << "MyStruct(int x)\n"s;
    }

    MyStruct(MyStruct&& obj) {
        cout << "Move\n"s;
    }

};


int main() {
    
    MyStruct obj( {12} ); // Неоднозначности нет
    MyStruct obj({ {12} }); // Неоднозначности нет

    return 0;
}

Если в конструкторе вместо int указать, например, тип string, то неоднозначность возникнет только в случае двойных фигурных скобок:

int main() {
    
    MyStruct obj( {"Hello"s} ); // Неоднозначности нет
    MyStruct obj({ {"Hello"s} }); // Ошибка из-за неоднозначности!

    return 0;
}

Вопросы:

  1. Почему при использовании int неоднозначности нет? Как мне казалось, фигурные скобки должны приводить к альтернативному пути инициализации, как это было в первом случае, но здесь этого не происходит.
  2. Почему при использовании string неоднозначность возникает только при двойных фигурных скобок?

Я понимаю, что в самом первом случае произошло неявное преобразование, но почему именно это повлияло на поведение?


Ответы (2 шт):

Автор решения: AP-JavaCod

Мне не понятно зачем вы используется такой синтаксис потому что при использовании Gadget g{10}; код будит работать так как вы верно отметили будит неявное приобразование типов. Если вы нехочете автоматического приаброзавания то можно использовать ключевое слово explicit.

Если таким образом вы хочете вызвать конструктор копирования то это является бесмыслиным так как у вас происходит два действия для создания одного объекта

  1. создаётс объекта
  2. копирование объекта.

Такой синтаксис работает не стабильно так как он не имеет стандартов.

→ Ссылка
Автор решения: sibedir

Когда мы пишем Gadget g(10), компилятор находит одну цепочку:
Gadget <- MyVector <- int.

- Проверяем Gadget(MyVector)
  - Можно ли 10 представить, как MyVector?
    - Проверяем MyVector(int)
      - Можно ли 10 представить int?
      - Да. Это точное соответствие.
  ? MyVector(int) - добавлен в кандидаты
  - (могу ошибаться, но кажется) Иные пути не рассматриваются, так как
    уже найдено точное совпадение.
  ! MyVector(int) - победивший кандидат
? Gadget(MyVector) - добавлен в кандидаты

- Проверяем Gadget(Gadget&&)
  - Gadget(Gadget&&) - оказалось рекурсией.
  - Прерываем ветку.

! Gadget(MyVector) - победивший кандидат

Если же напиcать Gadget g({10});, то есть уже 2 цепочки:
Gadget <- MyVector <- int
Gadget <- Gadget <- MyVector <- int
И вот почему

- Проверяем Gadget(MyVector)
  - Можно ли {10} представить, как MyVector?
  - Проверяем MyVector{10}
    ... (см. выше)
? Gadget(MyVector) - добавлен в кандидаты

- Проверяем Gadget(Gadget&&)
  - Можно ли {10} представить, как Gadget&&?
  - Проверяем Gadget{10}
    ... (см. выше)
? Gadget(Gadget&&) - добавлен в кандидаты

!!! ambiguous

Я пропущу кейс с MyStruct(int). Там компилятор всегда натыкается на точное соответствие int -> int причём всегда по пути только стандартных преобразований

Рассмотрим

struct MyStruct {
    MyStruct(string) { cout << "MyStruct(string)\n"; }
    MyStruct(MyStruct&&) { cout << "MyStruct(MyStruct&&)\n"; }
};

int main() {
    MyStruct obj1( "Hello"s ); // Неоднозначности нет
    MyStruct obj2( {"Hello"s} ); // Неоднозначности нет
    MyStruct obj3({ {"Hello"s} }); // Неоднозначность
    return 0;
}

obj1 - выбран MyStruct(string), так как "Hello"s точно соответствует string.

obj2 - выбран MyStruct(string), так как MyStruct(string{"Hello"s}) всё еще точно соответствует сигнатуре MyStruct(string). Чтобы пойти по пути MyStruct(MyStruct{"Hello"s}) придётся делать дополнительное преобразование типа.

obj3 - а вот тут уже начинается неоднозначность. Может имелось в виду MyStruct(string{string{"Hello"s}}). А может MyStruct(MyStruct{string{"Hello"s}}). Потому что для компилятора string -> string и MyStruct -> MyStruct - равновесные преобразования (а точнее отсутствие каких либо преобразований).

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

загадка от Жака Фреско

→ Ссылка