Спиральное правило о объявлениях - когда оно ошибочно?
Недавно я узнал правило спирали для деобобучения сложных объявлений, которые, должно быть, были написаны серией 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
.