Моя попытка инициализации значения интерпретируется как объявление функции, и почему A a(()); не решает ее?

#c #flatbuffers

#c

Вопрос:

Среди многих вещей, которым научило меня переполнение стека, это то, что известно как «самый неприятный синтаксический анализ», который классически демонстрируется с помощью такой строки, как

 A a(B()); //declares a function
 

Хотя для большинства интуитивно кажется, что это объявление объекта a типа A , принимающего временный B объект в качестве параметра конструктора, на самом деле это объявление функции a , возвращающей an A , принимая указатель на функцию, которая возвращает B и сама не принимает параметров. Аналогично строка

 A a(); //declares a function
 

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

 A a((B())); //declares an object
 

Однако во втором случае выполнение того же самого приводит к ошибке компиляции

 A a(()); //compile error
 

Мой вопрос в том, почему? Да, я очень хорошо знаю, что правильный «обходной путь» — это изменить его на A a; , но мне любопытно узнать, что extra () делает для компилятора в первом примере, который затем не работает при повторном применении его во втором примере. Является A a((B())); ли обходное решение конкретным исключением, записанным в стандарт?

Комментарии:

1. (B()) это просто выражение C , не более того. Это не какое-либо исключение. Единственное отличие, которое это делает, заключается в том, что его невозможно проанализировать как тип, и поэтому это не так.

2. Следует также отметить, что второй случай A a(); не относится к той же категории. Для компилятора никогда не существует другого способа его анализа: инициализатор в этом месте никогда не состоит из пустых круглых скобок, так что это всегда объявление функции.

3. отличный момент litb является тонким, но важным, и его стоит подчеркнуть — причина, по которой двусмысленность существует в этом объявлении ‘A a(B ())’, заключается в синтаксическом анализе ‘B()’ -> это может быть как выражение, так и объявление, и компилятор должен «выбрать»decl вместо expr — так что, если B() является decl, то ‘a’ может быть только функцией decl (не переменной decl). Если бы ‘()’ было разрешено быть инициализатором, ‘A a()’ был бы неоднозначным — но не expr против decl, а var decl против func decl — нет правила предпочесть один decl другому — и поэтому ‘()’ здесь просто не допускается в качестве инициализатора -и двусмысленность не возникает.

4. A a(); это не пример самого неприятного синтаксического анализа. Это просто объявление функции, как и в C.

5. «правильный «обходной путь» — изменить его на A a; » неверно. Это не даст вам инициализации типа POD. Чтобы получить инициализацию, напишите A a{}; .

Ответ №1:

Нет просвещенного ответа, это просто потому, что он не определен как допустимый синтаксис языком C … Так оно и есть, по определению языка.

Если у вас есть выражение внутри, тогда оно допустимо. Например:

  ((0));//compiles
 

Еще проще: because (x) является допустимым выражением C , в то время () как это не так.

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

Комментарии:

1. Еще проще: because (x) является допустимым выражением C , в то время () как это не так.

2. Я принял этот ответ, кроме того, комментарий Павла к моему первоначальному вопросу мне очень помог

Ответ №2:

Окончательное решение этой проблемы — перейти к единому синтаксису инициализации C 11, если вы можете.

 A a{};
 

http://www.stroustrup.com/C 11FAQ.html#uniform-инициализация

Ответ №3:

C деклараторы функций

Прежде всего, в C есть C. A a() объявление функции. Например, putchar имеет следующее объявление. Обычно такие объявления хранятся в заголовочных файлах, однако ничто не мешает вам написать их вручную, если вы знаете, как выглядит объявление функции. Имена аргументов являются необязательными в объявлениях, поэтому я опустил их в этом примере.

 int putchar(int);
 

Это позволяет вам писать код следующим образом.

 int puts(const char *);
int main() {
    puts("Hello, world!");
}
 

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

 #include <stdio.h>

int eighty_four() {
    return 84;
}

int output_result(int callback()) {
    printf("Returned: %dn", callback());
    return 0;
}

int main() {
    return output_result(eighty_four);
}
 

Как я уже упоминал, C позволяет опускать имена аргументов в заголовочных файлах, поэтому output_result в заголовочном файле они будут выглядеть так.

 int output_result(int());
 

Один аргумент в конструкторе

Разве вы не узнаете это? Что ж, позвольте мне напомнить вам.

 A a(B());
 

Да, это точно такое же объявление функции. A есть int , a есть output_result и B есть int .

Вы можете легко заметить конфликт языка Си с новыми возможностями C . Если быть точным, конструкторы — это имя класса и скобки, а альтернативный синтаксис объявления с () вместо = . По замыслу C пытается быть совместимым с кодом C, и поэтому ему приходится иметь дело с этим случаем — даже если практически никого это не волнует. Таким образом, старые функции C имеют приоритет перед новыми функциями C . Грамматика объявлений пытается сопоставить имя как функцию, прежде чем вернуться к новому синтаксису, () если это не удается.

Если бы одна из этих функций не существовала или имела другой синтаксис (например {} , в C 11), эта проблема никогда бы не возникла для синтаксиса с одним аргументом.

Теперь вы можете спросить, почему A a((B())) это работает. Что ж, давайте объявим output_result с бесполезными круглыми скобками.

 int output_result((int()));
 

Это не сработает. Грамматика требует, чтобы переменная не была заключена в круглые скобки.

 <stdin>:1:19: error: expected declaration specifiers or ‘...’ before ‘(’ token
 

Однако C ожидает здесь стандартного выражения. На C вы можете написать следующий код.

 int value = int();
 

И следующий код.

 int value = ((((int()))));
 

C ожидает, что выражение внутри круглых скобок будет… ну … выражение, в отличие от ожидаемого типа C. Скобки здесь ничего не значат. Однако, вставляя бесполезные круглые скобки, объявление функции C не сопоставляется, и новый синтаксис может быть сопоставлен должным образом (который просто ожидает выражения, например 2 2 ).

Дополнительные аргументы в конструкторе

Конечно, один аргумент хорош, но как насчет двух? Дело не в том, что конструкторы могут иметь только один аргумент. Одним из встроенных классов, который принимает два аргумента, является std::string

 std::string hundred_dots(100, '.');
 

Это все хорошо и прекрасно (технически, это было бы очень неприятно разобрать, если бы оно было написано как std::string wat(int(), char()) , но давайте будем честными — кто бы это написал? Но давайте предположим, что у этого кода есть досадная проблема. Вы бы предположили, что вам нужно заключить все в круглые скобки.

 std::string hundred_dots((100, '.'));
 

Не совсем так.

 <stdin>:2:36: error: invalid conversion from ‘char’ to ‘const char*’ [-fpermissive]
In file included from /usr/include/c  /4.8/string:53:0,
                 from <stdin>:1:
/usr/include/c  /4.8/bits/basic_string.tcc:212:5: error:   initializing argument 1 of ‘std::basic_string<_CharT, _Traits, _Alloc>::basic_string(const _CharT*, const _Allocamp;) [with _CharT = char; _Traits = std::char_traits<char>; _Alloc = std::allocator<char>]’ [-fpermissive]
     basic_string<_CharT, _Traits, _Alloc>::
     ^
 

Я не уверен, почему g пытается преобразовать char в const char * . В любом случае, конструктор был вызван только с одним значением типа char . Нет перегрузки, которая имеет один аргумент типа char , поэтому компилятор сбит с толку. Вы можете спросить — почему аргумент имеет тип char?

 (100, '.')
 

Да, , вот оператор запятой. Оператор запятой принимает два аргумента и выдает аргумент в правой части. Это не очень полезно, но это то, что нужно знать для моего объяснения.

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

 std::string hundred_dots((100), ('.'));
 

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

Нулевые аргументы в конструкторе

Возможно, вы заметили eighty_four функцию в моем объяснении.

 int eighty_four();
 

Да, на это также влияет самый неприятный синтаксический анализ. Это допустимое определение, и вы, скорее всего, видели его, если создавали заголовочные файлы (а вы должны). Добавление круглых скобок не исправляет это.

 int eighty_four(());
 

Почему это так? Ну, () это не выражение. В C вы должны поместить выражение в круглые скобки. Вы не можете писать auto value = () на C , потому () что это ничего не значит (и даже если бы это было так, как пустой кортеж (см. Python ), это был бы один аргумент, а не ноль). Практически это означает, что вы не можете использовать сокращенный синтаксис без использования синтаксиса C 11 {} , поскольку в нем нет выражений, которые можно заключить в круглые скобки, а грамматика C для объявлений функций всегда будет применяться.

Ответ №4:

Вы могли бы вместо этого

 A a(());
 

используйте

 A a=A();
 

Комментарии:

1. «Лучший обходной путь» не эквивалентен. int a = int(); инициализируется a с 0, int a; остается a неинициализированным. Правильный обходной путь — использовать A a = {}; для агрегатов, A a; когда инициализация по умолчанию делает то, что вы хотите, и A a = A(); во всех других случаях — или просто использовать A a = A(); последовательно. В C 11 просто используйте A a {};

Ответ №5:

Самыми внутренними скобками в вашем примере будет выражение, а в C грамматика определяет an expression как an assignment-expression или another expression , за которым следует запятая и другое assignment-expression (Приложение A.4 — Краткое изложение грамматики / выражения).

Грамматика далее определяет an assignment-expression как один из нескольких других типов выражений, ни одно из которых не может быть ничем (или только пробелом).

Итак, причина, по которой вы не можете A a(()) этого сделать, заключается просто в том, что грамматика этого не позволяет. Однако я не могу ответить, почему люди, создавшие C , не разрешили это конкретное использование пустых скобок в качестве своего рода особого случая — я бы предположил, что они предпочли бы не вводить такой особый случай, если бы была разумная альтернатива.