Спиральное правило о объявлениях - когда оно ошибочно?

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

Часто цитируемое упрощение, которое работает только для нескольких простых случаев.

Я не нахожу void (*signal(int, void (*fp)(int)))(int); "простой случай". Между тем, тем более тревожно.

Итак, мой вопрос в том, в каких ситуациях я буду правильно применять правило и в котором он был бы ошибкой?

Ответ 1

В принципе, правило просто не работает, иначе оно работает, переопределяя то, что подразумевается под спиралью (в этом случае, в этом нет никакого смысла. Рассмотрим, например:

int* a[10][15];

Правило спирали даст a - массив [10] указателя на array [15] из int, что неверно. Это случай, когда вы на сайте, он тоже не работает; на самом деле, в случае signal, это не даже ясно, где вы должны начать спираль.

В общем, легче найти примеры того, где правило не выполняется чем примеры, где он работает.

У меня часто возникает соблазн сказать, что синтаксический анализ объявления С++ простой, но ни один орган, который пытался со сложными заявлениями Поверь мне. С другой стороны, это не так сложно, как это иногда это делается. Секрет в том, чтобы думать о так же, как и выражение, но с большим количеством меньше операторов и очень простое правило приоритета: все операторы справа имеют приоритет над всеми операторами слева. В отсутствие круглых скобок, это означает, что все сначала сначала, затем все влево, и процесс в скобках точно так же, как и в любом другом выражении. Фактическая трудность - это не синтаксис как таковой, а то, что он результаты - это очень сложные и противоречивые декларации, в частности, где возвращаемые значения функции и указатели на функции: первое правое, затем левое правило означает что операторы на определенном уровне часто широко разделены, например:.

int (*f( /* lots of parameters */ ))[10];

Последний член в расширении здесь int[10], но [10] после полной спецификации функции (при по крайней мере для меня) очень неестественно, и я должен остановиться и разобраться каждый раз. (Вероятно, эта тенденция для логически смежных частей для распространения, которые приводят к правилу спирали. Проблема конечно, что в отсутствие круглых скобок они не всегда выкладывается — в любое время, когда вы видите [i][j], правило идет справа, затем снова идите направо, а не спираль.)

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

Ответ 2

Правило правильное. Тем не менее, следует очень осторожно применять его.

Я предлагаю применить его более формально для объявлений C99 +.

Самое главное здесь - признать следующую рекурсивную структуру всех объявлений (const, volatile, static, extern, inline, struct, union, typedef удаляются из изображения для простоты, но могут быть легко добавлены обратно):

base-type [derived-part1: *'s] [object] [derived-part2: [] or ()]

Да, это он, четыре части.

where

  base-type is one of the following (I'm using a bit compressed notation):
    void
    [signed/unsigned] char
    [signed/unsigned] short [int]
    signed/unsigned [int]
    [signed/unsigned] long [long] [int]
    float
    [long] double
    etc

  object is
      an identifier
    OR
      ([derived-part1: *'s] [object] [derived-part2: [] or ()])

  * is *, denotes a reference/pointer and can be repeated
  [] in derived-part2 denotes bracketed array dimensions and can be repeated
  () in derived-part2 denotes parenthesized function parameters delimited with ,'s
  [] elsewhere denotes an optional part
  () elsewhere denotes parentheses

После того, как вы проанализировали все 4 части,

  [object] является [ derived-part2 (содержащий/возвращающий)] [derived-part2 (указатель на)] base-type 1.

Если есть рекурсия, вы найдете свой object (если есть) в нижней части стека рекурсии, он будет самым внутренним, и вы получите полную декларацию, вернувшись и собирая и объединение полученных частей на каждом уровне рекурсии.

Во время разбора вы можете переместить [object] на [derived-part2] (если есть). Это даст вам линеаризованное, легко понятное объявление (см. Выше 1).

Таким образом, в

char* (**(*foo[3][5])(void))[7][9];

вы получаете:

  • base-type= char
  • уровень 1: derived-part1= *, object= (**(*foo[3][5])(void)), derived-part2= [7][9]
  • уровень 2: derived-part1= **, object= (*foo[3][5]), derived-part2= (void)
  • уровень 3: derived-part1= *, object= foo, derived-part2= [3][5]

Оттуда:

  • уровень 3: * [3][5] foo
  • Уровень 2: ** (void) * [3][5] foo
  • уровень 1: * [7][9] ** (void) * [3][5] foo
  • наконец, char * [7][9] ** (void) * [3][5] foo

Теперь, читая справа налево:

foo представляет собой массив из 3 массивов из 5 указателей на функцию (без параметров), возвращающую указатель на указатель на массив из 7 массивов из 9 указателей на char.

Вы можете изменить размеры массива в каждом derived-part2 в этом процессе.

Это ваше спиральное правило.

И легко видеть спираль. Вы погружаетесь в все более глубоко вложенные [object] слева, а затем всплываете справа, только чтобы заметить, что на верхнем уровне есть еще пара левых и правых и т.д.

Ответ 3

например:.

int * a[][5];

Это не массив указателей на массивы int.