Неоднозначное разрешение перегрузки с параметром initializer_list

Я тестировал следующий код в Visual Studio, и он компилирует и печатает "A (double)".

#include <iostream>
#include <initializer_list>

struct A {
    A(std::initializer_list<int>) { puts("initializer_list<int>"); }        // (1)
    A(std::initializer_list<float>) { puts("initializer_list<float>"); }    // (2)
    A(double) { puts("A(double)"); }                                        // (3)
};

int main() {
    A var{ 1.1 };   
}

Однако как IntelliSense, так и http://ideone.com/ZS1Mcm не согласны, говоря, что более одного экземпляра конструктора "A:: A" соответствует списку аргументов ( что означает как конструкторы-инициализаторы-списки). Обратите внимание, что если удаляются либо (1), либо (2), код больше не компилируется, поскольку "преобразование из" double "в" float "требует сужения преобразования".

Это ошибка? Поведение кажется непоследовательным, но я вижу такое же поведение в VS13 и VS15, так что, может быть, есть больше?

Ответ 1

Код плохо сформирован. Применяется §8.5.4/(3.6):

В противном случае, если T - тип класса, рассматриваются конструкторы. применимые конструкторы перечислены и выбран лучший через разрешение перегрузки (13.3, 13.3.1.7).

Теперь, §13.3.3.1.5 идет

Когда аргумент представляет собой список инициализаторов (8.5.4), это не выражение и специальные правила для преобразования это к типу параметра. [...] если тип параметра std::initializer_list<X> и все элементы списка инициализаторов могут быть неявно преобразованы в X, неявная последовательность преобразований является наихудшим преобразованием, необходимым для преобразовать элемент списка в X, или если в списке инициализаторов нет элементов, преобразование идентичности.

Преобразование 1.1, которое имеет тип double (!), int - это преобразование с плавающим интегралом с рангом конверсии, а преобразование с 1.1 в float - это преобразование с плавающей точкой - также имеющий ранг конверсии.

введите описание изображения здесь

Таким образом, оба преобразования одинаково хороши, и поскольку § 13.3.3.2/(3.1) также не могут их отличить, вызов неоднозначен. Обратите внимание, что сужение не играет роли до тех пор, пока не будет выполнено разрешение перегрузки и, следовательно, не может повлиять на набор кандидатов или процесс выбора. Точнее, кандидат должен соответствовать требованию, установленному в 13.3.2/3:

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

Однако, как показано во второй цитате, неявная последовательность преобразования, которая преобразует {1.1} в std::initializer_list<int>, является наихудшим преобразованием от 1.1 до int, которое является преобразованием с плавающим интегралом - и действительным ( и существующий!) один на этом.


Если вместо этого вы передадите {1.1f} или измените значение initializer_list<float> на <double>, код будет корректным, так как преобразование 1.1f в float является преобразованием идентичности. Стандарт дает соответствующий пример в (3.6):

[Пример:

struct S {
    S(std::initializer_list<double>); // #1
    S(std::initializer_list<int>);    // #2

};
S s1 = { 1.0, 2.0, 3.0 }; // invoke #1

- конец примера]

Еще интереснее,

struct S {
    S(std::initializer_list<double>); // #1
    S(std::initializer_list<int>);    // #2

};
S s1 = { 1.f }; // invoke #1 

Также действителен - поскольку преобразование с 1.f в double является плавающей точкой promotion, имея рейтинг продвижения, который лучше, чем рейтинг конверсии.

Ответ 2

Visual Studio ошибочно принимает ваш код - он плохо сформирован и не должен компилироваться. gcc и clang являются правильными в отказе от него.

Ключевая точка маркера в инициализации списка здесь:

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

Для чего мы ссылаемся на [over.match.list]:

Когда объекты неагрегатного типа типа T инициализируются списком, так что 8.5.4 указывает, что разрешение перегрузки выполняется в соответствии с правилами в этом разделе, разрешение перегрузки выбирает конструктор в две фазы:
- Изначально кандидатские функции являются конструкторами-инициализаторами (8.5.4) класса T и Список аргументов состоит из списка инициализаторов как один аргумент.

У нас есть две такие кандидатные функции - (1) и (2). Ни один из них не лучше, чем другой, поскольку оба преобразования от double до int и float имеют ранг конверсии. Таким образом, вызов неоднозначен. Обратите внимание, что даже если есть кандидат с Exact Rank (A(double )), сначала рассмотрим конструкторы initializer_list.