Насколько я могу судить, это ошибка clang.
Инициализация списка копирования имеет довольно неинтуитивное поведение: он считает явные конструкторы жизнеспособными до тех пор, пока разрешение перегрузки не будет полностью завершено, но может отклонить результат перегрузки, если выбран явный конструктор. Формулировка в проекте post-N4567 [over.match.list] p1
В инициализации списка копий, если выбран конструктор
blockquote>explicit
, инициализация плохо сформирована. [ Примечание: Это отличается от других ситуаций (13.3.1.3, 13.3.1.4), где рассматриваются только конструкторы преобразования для инициализации копирования. Это ограничение применяется только в том случае, если эта инициализация является частью окончательного результата разрешения перегрузки. - end note ]clang HEAD принимает следующую программу:
#include
using namespace std; struct String1 { explicit String1(const char*) { cout << "String1\n"; } }; struct String2 { String2(const char*) { cout << "String2\n"; } }; void f1(String1) { cout << "f1(String1)\n"; } void f2(String2) { cout << "f2(String2)\n"; } void f(String1) { cout << "f(String1)\n"; } void f(String2) { cout << "f(String2)\n"; } int main() { //f1( {"asdf"} ); f2( {"asdf"} ); f( {"asdf"} ); } Которая, за исключением комментирования звоните в
f1
, прямо из N2532 Bjarne Stroustrup - Унифицированная инициализация , глава 4. Благодаря Johannes Schaub для показа мне эту статью на std-обсуждение .В той же главе содержится следующее объяснение:
Реальное преимущество
blockquote>explicit
в том, что оно делает ошибкуf1("asdf")
ошибкой. Проблема в том, что разрешение перегрузки «предпочитает» конструкторы неexplicit
, так чтоf("asdf")
вызываетf(String2)
. Я считаю, что разрешениеf("asdf")
меньше идеала, потому что авторString2
, вероятно, не имел целью разрешить двусмысленности в пользуString2
(по крайней мере, не в каждом случае, когда явные и неявные конструкторы происходят так) и писательString1
, конечно, этого не делал. Правило благоприятствует «неаккуратным программистам», которые не используютexplicit
.Насколько мне известно, N2640 - Списки инициализаторов - Альтернативный механизм и обоснование - последняя статья, которая включает в себя обоснование такого разрешения перегрузки; его преемник N2672 был проголосован в черновик C ++ 11.
Из его главы «Значение явного»:
Первый подход для того чтобы сделать пример плохо сформированным, требуется, чтобы все конструкторы (явные и неявные) рассматривались для неявных преобразований, но если выбран явный конструктор, эта программа плохо сформирована. Это правило может ввести свои собственные сюрпризы; например:
struct Matrix { explicit Matrix(int n, int n); }; Matrix transpose(Matrix); struct Pixel { Pixel(int row, int col); }; Pixel transpose(Pixel); Pixel p = transpose({x, y}); // Error.
Второй подход заключается в том, чтобы игнорировать явные конструкторы при поиске жизнеспособности неявного преобразования, но включать их при фактическом выборе конструктора преобразования: если явный конструктор заканчивается выбранный, программа плохо сформирована. Этот альтернативный подход позволяет последнему примеру (Pixel-vs-Matrix) работать как ожидалось (выбран
blockquote>transpose(Pixel)
), в то время как исходный пример («X x4 = { 10 };
») плохо сформирован.Хотя в этом документе предлагается использовать второй подход, его формулировка представляется ошибочной - в моей интерпретации формулировки она не приводит к поведению, изложенному в обоснованной части статьи. Формулировка пересмотрена в N2672, чтобы использовать первый подход, но я не мог найти никакого обсуждения о том, почему это было изменено.
Конечно, немного больше формулировок, связанных с инициализацией переменной, как в OP, но учитывая разницу в поведении между clang и gcc, то же самое для первой программы-образца в моем ответе, я думаю, что это касается основных моментов.
Это потому, что b + = 1.0;
эквивалентно b = (int) ((b) + (1.0) );
. Сужающее примитивное преобразование (JLS 5.1.3) скрыто в операции составного присваивания.
Выражение составного присваивания в форме E1 op = E2 эквивалентно E1 = (T) ((E1) op (E2)) , где T - это тип E1 , за исключением того, что E1 оценивается только один раз.
Например, следующий код верен:
short x = 3; x + = 4.6;
и приводит к
x
, имеющему значение7
, потому что оно эквивалентно:short x = 3; x = (short) (x + 4.6);
Это также объясняет, почему следующий код компилируется:
byte b = 1;
int x = 5;
b += x; // compiles fine!
Но этого не происходит:
byte b = 1;
int x = 5;
b = b + x; // DOESN'T COMPILE!
В этом случае вам необходимо явное приведение:
byte b = 1;
int x = 5;
b = (byte) (b + x); // now it compiles fine!
Стоит отметить, что неявное приведение в составных присваиваниях является предметом Головоломки 9: Tweedledum из замечательной книги Java Puzzlers .Вот отрывок из книги (слегка отредактированный для краткости):
Многие программисты думают, что
x + = i;
- это просто сокращение дляx = x + i;
. Это не совсем так: если тип результата шире, чем тип переменной, оператор составного присваивания выполняет тихое сужающее примитивное преобразование.Чтобы избежать неприятных сюрпризов, не используйте составные операторы присваивания для переменных типа
byte
,short
илиchar
. При использовании составных операторов присваивания для переменных типаint
убедитесь, что выражение в правой части не относится к типуlong
,float
или] двойной
. При использовании составных операторов присваивания для переменных типаfloat
убедитесь, что выражение в правой части не относится к типуdouble
. Этих правил достаточно, чтобы компилятор не генерировал опасные сужающие приведения.Для разработчиков языков, вероятно, будет ошибкой составные операторы присваивания генерировать невидимые приведения; составные присваивания, в которых переменная имеет более узкий тип, чем результат вычисления, вероятно, должны быть недопустимыми.
Последний абзац стоит отметить: C # намного строже в этом отношении (см. Спецификация языка C # 7.13.2 Составное присваивание ).