Почему это не скомпилируется без конструктора по умолчанию?

Я могу сделать это:

#include <iostream>

int counter;

int main()
{
    struct Boo
    {
        Boo(int num)
        {
            ++counter;
            if (rand() % num < 7) Boo(8);
        }
    };

    Boo(8);

    return 0;
}

Это скомпилируется нормально, мой счетчик результат 21. Однако, когда я пытаюсь создать объект Boo передавая аргумент конструктора вместо целочисленного литерала, я получаю ошибку компиляции:

#include <iostream>

int counter;

int main()
{
    struct Boo
    {
        Boo(int num)
        {
            ++counter;
            if (rand() % num < 7) Boo(num); // No default constructor 
                                            // exists for Boo
        }
    };

    Boo(8);

    return 0;
}

Как вызывается конструктор по умолчанию во втором примере, а не в первом? Это ошибка, которую я получаю на Visual Studio 2017.

В онлайн компиляторе C++ onlineGDB я получаю ошибки:

error: no matching function for call to ‘main()::Boo::Boo()
    if (rand() % num < 7) Boo(num);

                           ^
note:   candidate expects 1 argument, 0 provided

Ответы

Ответ 1

Clang выдает это предупреждение:

<source>:12:16: warning: parentheses were disambiguated as redundant parentheses around declaration of variable named 'num' [-Wvexing-parse]
            Boo(num); // No default constructor 
               ^~~~~

Это самая неприятная проблема разбора. Поскольку Boo - это имя типа класса, а num - это не имя типа, Boo(num); это может быть либо конструкция временного типа Boo с аргументом num для конструктора Boo либо это может быть объявление Boo num; с дополнительными скобками вокруг декларатора num (который всегда может иметь декларатор). Если оба являются допустимыми интерпретациями, стандарт требует, чтобы компилятор принял объявление.

Если он анализируется как объявление, то Boo num; вызовет конструктор по умолчанию (конструктор без аргументов), который не объявлен ни вами, ни неявно (потому что вы объявили другой конструктор). Поэтому программа плохо сформирована.

Это не проблема с Boo(8); потому что 8 не может быть идентификатором переменной (декларатор-идентификатор), поэтому он анализируется как вызов, создающий Boo временный с 8 качестве аргумента для конструктора, тем самым не вызывая конструктор по умолчанию (который не объявлен), но тот, который вы определяется вручную.

Вы можете избавиться от неоднозначности этого из объявления, используя Boo{num}; вместо Boo(num); (потому что {} вокруг объявления не допускается), делая временную переменную именованной, например Boo temp(num); или путем помещения его в качестве операнда в другое выражение, например (Boo(num)); , (void)Boo(num); , так далее.

Обратите внимание, что объявление было бы правильно сформировано, если бы был использован конструктор по умолчанию, потому что оно находится внутри области действия ветки if а не в области функциональных блоков и просто затеняет num в списке параметров функции.

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

Этот конкретный тип наиболее неприятного анализа с одним не типичным именем в круглых скобках может произойти только потому, что целью является создание временного объекта и немедленное его удаление, или, в качестве альтернативы, если это намерение создать временный объект, используемый непосредственно в качестве инициализатора, например Boo boo(Boo(num)); (фактически объявляет функцию boo принимающую параметр с именем num с типом Boo и возвращающую Boo).

Немедленное отбрасывание временных значений обычно не предназначено, и можно избежать случая инициализатора, используя инициализацию скобок или двойные паразиты (Boo boo{Boo(num)}, Boo boo(Boo{num}) или Boo boo((Boo(num))); но не Boo boo(Boo((num)));).

Если Boo не является именем типа, оно не может быть объявлением, и никаких проблем не возникает.

Я также хочу подчеркнуть, что Boo(8); создает новый временный объект типа Boo даже внутри области видимости класса и определения конструктора. Это не, как можно ошибочно подумать, вызов конструктора с вызывающим this указателем, как для обычных нестатических функций-членов. Невозможно вызвать другой конструктор таким образом внутри тела конструктора. Это возможно только в списке инициализатора члена конструктора.


Это происходит, даже если объявление будет неправильно сформировано из-за отсутствия конструктора из-за [stmt.ambig]/3:

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

[...]

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


Исправлено в редактировании: я пропустил рассматриваемое объявление, находящееся в другой области видимости, чем параметр функции, и поэтому объявление было правильно сформировано, если был доступен конструктор. Это не учитывается при устранении неоднозначности в любом случае. Также раскрыты некоторые детали.

Ответ 2

Это известно как самый неприятный анализ (этот термин был использован Скоттом Мейерсом в Effective STL).

Boo(num) не вызывает конструктор и не создает временный. Clang дает хорошее предупреждение, чтобы увидеть (даже с правильным именем Wvexing-parse):

<source>:12:38: warning: parentheses were disambiguated as redundant parentheses around declaration of variable named 'num' [-Wvexing-parse]

То, что видит компилятор, эквивалентно

Boo num;

который является переменным decleration. Вы объявили переменную Boo с именем num, для которой нужен конструктор по умолчанию, даже если вы хотели создать временный объект Boo. Стандарт c++ требует, чтобы компилятор в вашем случае предполагал, что это объявление переменной. Теперь вы можете сказать: "Эй, num - это int, не делай этого". Тем не менее, стандарт гласит:

Неоднозначность является чисто синтаксической; то есть значение имен, встречающихся в таком утверждении, вне зависимости от того, являются ли они именами типов или нет, обычно не используется в неоднозначности или не изменяется. Шаблоны классов создаются по мере необходимости, чтобы определить, является ли квалифицированное имя именем типа. Устранение неоднозначности предшествует синтаксическому анализу, и утверждение, неоднозначное как декларация, может быть неправильно сформированной декларацией. Если во время синтаксического анализа имя в параметре шаблона связано не так, как оно было бы во время пробного анализа, программа некорректна. Диагностика не требуется. [Примечание: это может произойти, только если имя объявлено ранее в объявлении. - конец примечания]

Так что выхода из этого нет.

Для Boo(8) это не может произойти, так как анализатор может быть уверен, что это не декларация (8 не является допустимым именем идентификатора) и вызывает конструктор Boo(int).

Кстати: вы можете устранить неоднозначность, используя заключающие в скобки:

 if (rand() % num < 7)  (Boo(num));

или, на мой взгляд, лучше использовать новый единый синтаксис инициализации

if (rand() % num < 7)  Boo{num};

Который потом будет компилировать, см. Здесь и здесь.

Ответ 3

Вот лягушатник предупреждение

truct_init.cpp: 11:11: ошибка: переопределение 'num' с другим типом: 'Boo' против 'int'

Ответ 4

Пользователь 10605163 наблюдает:

Clang выдает это предупреждение:

Я разработал аналогичную диагностику для g++ более десяти лет назад.

Разработчики в организации, в которой я работал в то время, были укушены этим несколько раз.