Подтвердить что ты не робот

Вызов явного конструктора со списком braced-init: неоднозначный или нет?

Рассмотрим следующее:

struct A {
    A(int, int) { }
};

struct B {
    B(A ) { }                   // (1)
    explicit B(int, int ) { }   // (2)
};

int main() {
    B paren({1, 2});   // (3)
    B brace{1, 2};     // (4)
}

Конструкция brace в (4) ясно и недвусмысленно называет (2). В clang конструкция paren в (3) однозначно вызывает (1), где, как и в gcc 5.2, она не скомпилируется с помощью:

main.cpp: In function 'int main()':
main.cpp:11:19: error: call of overloaded 'B(<brace-enclosed initializer list>)' is ambiguous
     B paren({1, 2});
                   ^
main.cpp:6:5: note: candidate: B::B(A)
     B(A ) { }  
     ^
main.cpp:5:8: note: candidate: constexpr B::B(const B&)
 struct B {
        ^
main.cpp:5:8: note: candidate: constexpr B::B(B&&)

Какой компилятор прав? Я подозреваю, что clang здесь верен, так как двусмысленность в gcc может возникнуть только через путь, который подразумевает неявное построение B{1,2} и передачу этого в конструктор copy/move, но этот конструктор отмечен как explicit, поэтому такая неявная конструкция не должна допустимо.

4b9b3361

Ответ 1

Насколько я могу судить, это ошибка clang.

Инициализация экземпляра копирования имеет довольно неинтуитивное поведение: он считает явные конструкторы жизнеспособными до тех пор, пока разрешение перегрузки не будет полностью завершено, но затем может отклонить результат перегрузки, если выбран явный конструктор. Формулировка в проекте после N4567, [over.match.list] p1

В инициализации списка копий, если выбран конструктор explicit, инициализация плохо сформирована. [Примечание: это отличается от других ситуаций (13.3.1.3, 13.3.1.4), где только преобразования конструкторов рассматриваются для инициализации копирования. Это ограничение применяется только если эта инициализация является частью конечного результата перегрузки разрешающая способность. - конечная нота]


clang HEAD принимает следующую программу:

#include <iostream>
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, прямо из Bjarne Stroustrup N2532 - Унифицированная инициализация, глава 4. Спасибо на Йоханнес Шауб, чтобы показать мне эту статью на std-discussion.

В той же главе содержится следующее объяснение:

Реальным преимуществом explicit является то, что он отображает f1("asdf")ошибка. Проблема в том, что разрешение перегрузки "предпочитает" не explicitконструкторы, так что f("asdf") вызывает f(String2). Я считаю, что разрешение f("asdf") меньше идеала, потому что автор String2, вероятно, не означал разрешения двусмысленностей в пользу String2 (по крайней мере, не в каждом случае, когда явные и неявные конструкторы происходят так) и писатель String1 конечно техника его подводит. Правило благоприятствует "неаккуратным программистам", которые не используют explicit.


Насколько мне известно, N2640 - Списки инициализаторов - Альтернативный механизм и Обоснование - последняя статья, которая включает в себя обоснование такого разрешения перегрузки; его преемник N2672 был проголосован в проекте С++ 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) как ожидалось (transpose(Pixel)), при этом оригинальный пример ( "X x4 = { 10 };" ) плохо сформирован.

Хотя в этом документе предлагается использовать второй подход, его формулировка представляется ошибочной - в моей интерпретации формулировки она не приводит к поведению, изложенному в обоснованной части статьи. Формулировка пересмотрена в N2672 для использования первого подхода, но я не мог найти никакого обсуждения о том, почему это было изменено.


Есть, конечно, немного больше формулировок, связанных с инициализацией переменной, как в OP, но учитывая разницу в поведении между clang и gcc, то же самое для первой примерной программы в моем ответе, я думаю, что это касается основных моментов.

Ответ 2

Это не полный ответ, хотя он слишком длинный, как комментарий.
Я постараюсь предложить контрпример к вашим рассуждениям, и я готов видеть downvote, потому что я далек от того, чтобы быть уверенным.
Во всяком случае, пусть попробует!!: -)

Это следует из приведенного примера:

struct A {
    A(int, int) { }
};

struct B {
    B(A) { }
    explicit B(int, int ) { }
};

int main() {
    B paren({1, 2});
}

В этом случае утверждение {1, 2} дает, по-видимому, два решения:

  • прямая инициализация с помощью B(A), потому что A(int, int) не является явным и, следовательно, разрешена и что фактически первый кандидат

  • по той же причине, что и выше, его можно интерпретировать как B{B(A{1,2})} (ну, позвольте мне использовать обозначение, чтобы дать вам представление и что я имею в виду), то есть {1,2} позволяет построить a B временный объект, который используется сразу после аргумента для конструктора copy/move, и он снова разрешен, поскольку задействованные конструкторы не являются явными

Последний объяснил бы второго и третьего кандидатов.

Имеет ли смысл?
Я готов удалить ответы до тех пор, пока вы объясните мне, что не так в моих рассуждениях.: -)