С не так сложно: void (* (* f []) ()) ()

188

Я только сегодня увидел фотографию и думаю, что буду благодарен за объяснения. Итак, вот картинка:

некоторый код c

Я нашел это смущающим и задавался вопросом, практичны ли когда-либо такие коды. Я гуглил картинку и нашел другую картинку в этой записи Reddit, и вот эта картинка:

какое-то интересное объяснение

Так что это «чтение по спирали» является чем-то действительным? Это как C-компиляторы анализируют?
Было бы здорово, если бы есть более простые объяснения этого странного кода.
Кроме всего прочего, могут ли эти коды быть полезными? Если да, то где и когда?

Есть вопрос о «спиральном правиле», но я не просто спрашиваю о том, как оно применяется или как выражения читаются с этим правилом. Я подвергаю сомнению использование таких выражений и правильность спирального правила. Что касается этого, некоторые хорошие ответы уже опубликованы.

Motun
источник
9
Как объяснение может быть проще? Он охватывает все аспекты определения fw / пары слов для каждого ключевого момента.
Скотт Хантер
30
Может быть, это сложно? Это фактически объявляет fкак массив указателей на функции, которые могут принимать любой аргумент ... если бы это было так void (*(*f[])(void))(void);, тогда да, это были бы функции, которые не принимают аргументов ...
txtechhelp
18
На практике не используйте такой неясный код. Используйте typedef для подписей
Василий Старынкевич
4
любое объявление с указателями на функции может быть сложным. Это не значит, что нормальный C или C ++ сложен таким же образом. Другие языки решают эту проблему по-разному, включая отсутствие указателей на функции, что в некоторых случаях может быть существенным упущением
Кейт Грегори
20
Если вы прищурите глаза, это будет похоже на LISP.
user2023861 31.12.15

Ответы:

117

Существует правило, называемое «Правило по часовой стрелке / спираль», которое помогает найти значение сложного объявления.

Из c-faq :

Есть три простых шага:

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

    [X]or []
    => Array X size of ... или Array undefined size of ...

    (type1, type2)
    => функция, передающая type1 и type2, возвращающая ...

    *
    => указатель (и) на ...

  2. Продолжайте делать это по спирали / по часовой стрелке, пока все жетоны не будут закрыты.

  3. Всегда сначала разрешите что-нибудь в скобках!

Вы можете проверить ссылку выше для примеров.

Также обратите внимание, что в помощь вам также существует сайт под названием:

http://www.cdecl.org

Вы можете ввести декларацию C, и она придаст смысл английскому языку. Для

void (*(*f[])())()

это выводит:

объявить f как массив указателей на функцию, возвращающую указатель на функцию, возвращающую void

РЕДАКТИРОВАТЬ:

Как указано в комментариях Random832 , спиральное правило не обращается к массиву массивов и приведет к неправильному результату (большей части) этих объявлений. Например, для int **x[1][2];спирального правила игнорируется тот факт, что []имеет более высокий приоритет над *.

Перед массивом массивов можно сначала добавить явные скобки перед применением правила спирали. Например: int **x[1][2];то же самое, что и int **(x[1][2]);(также допустимый C) из-за приоритета, и тогда спиральное правило правильно его читает, поскольку «x - это массив 1 из массива 2 указателя на указатель на int», который является правильным объявлением на английском языке.

Обратите внимание , что этот вопрос также рассматривается в этом ответе по Джеймс Kanze (указал haccks в комментариях).

ouah
источник
5
Хотелось бы, чтобы cdecl.org был лучше
Grady Player
8
Не существует "спирального правила" ... "int *** foo [] [] []" определяет массив массивов массивов указателей на указатели на указатели. «Спираль» происходит только от того факта, что эта декларация сгруппировала вещи в скобках таким образом, что они стали чередоваться. Это все справа, потом слева, внутри каждого набора скобок.
Random832
1
@ Random832 Существует «спиральное правило», и оно охватывает только что упомянутый вами случай, то есть говорит о том, как обращаться с круглыми скобками / массивами и т. Д. Конечно, это не стандартное правило C, а хорошая мнемоника для выяснения, как действовать со сложными декларациями. ИМХО, это чрезвычайно полезно и спасает вас, когда у вас проблемы или когда cdecl.org не может разобрать объявление. Конечно, не следует злоупотреблять такими заявлениями, но хорошо знать, как они анализируются.
vsoftco
5
@vsoftco Но это не «движение по спирали / по часовой стрелке», если вы только поворачиваетесь, когда достигаете скобок.
Random832
2
Оу, вы должны упомянуть, что спиральное правило не является универсальным .
хак
105

Вид «спирального» правила выпадает из следующих правил приоритета:

T *a[]    -- a is an array of pointer to T
T (*a)[]  -- a is a pointer to an array of T
T *f()    -- f is a function returning a pointer to T
T (*f)()  -- f is a pointer to a function returning T

Операторы []вызова индекса и функции ()имеют более высокий приоритет, чем унарные *, поэтому *f()анализируются как *(f())и *a[]анализируются как *(a[]).

Так что если вы хотите указатель на массив или указатель на функцию, то вам нужно явно сгруппировать *с идентификатором, как в (*a)[]или (*f)().

Тогда вы понимаете это aи fможете быть более сложными выражениями, чем просто идентификаторы; in T (*a)[N], aможет быть простым идентификатором, или это может быть вызов функции вроде (*f())[N]( a-> f()), или это может быть массив типа (*p[M])[N]( a-> p[M]), или это может быть массив указателей на функции вроде (*(*p[M])())[N]( a-> (*p[M])()), и т.п.

Было бы неплохо, если бы оператор косвенности *был постфиксным, а не унарным, что могло бы несколько облегчить чтение деклараций слева направо ( void f[]*()*();безусловно, лучше, чем void (*(*f[])())()), но это не так.

Когда вы сталкиваетесь с таким волосатым объявлением, начните с поиска крайнего левого идентификатора и примените приведенные выше правила приоритета, рекурсивно применяя их к любым параметрам функции:

         f              -- f
         f[]            -- is an array
        *f[]            -- of pointers  ([] has higher precedence than *)
       (*f[])()         -- to functions
      *(*f[])()         -- returning pointers
     (*(*f[])())()      -- to functions
void (*(*f[])())();     -- returning void

signalФункция в стандартной библиотеке, вероятно , образец типа для этого вида безумия:

       signal                                       -- signal
       signal(                          )           -- is a function with parameters
       signal(    sig,                  )           --    sig
       signal(int sig,                  )           --    which is an int and
       signal(int sig,        func      )           --    func
       signal(int sig,       *func      )           --    which is a pointer
       signal(int sig,      (*func)(int))           --    to a function taking an int                                           
       signal(int sig, void (*func)(int))           --    returning void
      *signal(int sig, void (*func)(int))           -- returning a pointer
     (*signal(int sig, void (*func)(int)))(int)     -- to a function taking an int
void (*signal(int sig, void (*func)(int)))(int);    -- and returning void

В этот момент большинство людей говорят «используйте typedefs», что, безусловно, является опцией:

typedef void outerfunc(void);
typedef outerfunc *innerfunc(void);

innerfunc *f[N];

Но...

Как бы вы использовали f в выражении? Вы знаете, что это массив указателей, но как вы используете его для выполнения правильной функции? Вы должны просмотреть typedefs и найти правильный синтаксис. Напротив, «голая» версия довольно приятна, но она точно говорит вам, как использовать f в выражении (а именно (*(*f[i])())();, если предположить, что ни одна функция не принимает аргументы).

Джон Боде
источник
7
Спасибо за пример «сигнала», показывающий, что такого рода вещи действительно появляются в дикой природе.
Justsalt
Это отличный пример.
Кейси
Мне понравилось ваше fдерево замедления, объясняющее приоритет ... почему-то я всегда получаю
удовольствие
1
при условии, что ни одна из функций не принимает аргументов : тогда вы должны использовать voidв скобках функции, в противном случае она может принимать любые аргументы.
хак
1
@haccks: для декларации да; Я говорил о вызове функции.
Джон Боде
57

В C декларация отражает использование - это то, как это определено в стандарте. Декларация:

void (*(*f[])())()

Утверждение, что выражение (*(*f[i])())()выдает результат типа void. Что значит:

  • f должен быть массивом, так как вы можете индексировать его:

    f[i]
  • Элементы fдолжны быть указателями, так как вы можете разыменовать их:

    *f[i]
  • Эти указатели должны быть указателями на функции без аргументов, так как вы можете вызывать их:

    (*f[i])()
  • Результаты этих функций также должны быть указателями, так как вы можете разыменовать их:

    *(*f[i])()
  • Эти указатели также должны быть указателями на функции без аргументов, так как вы можете вызывать их:

    (*(*f[i])())()
  • Эти указатели функций должны возвращаться void

«Правило спирали» - это просто мнемоника, которая дает другой способ понимания одного и того же.

Джон Перди
источник
3
Отличный способ смотреть на это, чего я никогда не видел прежде. +1
августа
4
Ницца. С этой точки зрения это действительно просто . На самом деле довольно проще, чем что-то вроде vector< function<function<void()>()>* > f, особенно если добавить в std::с. (Но хорошо, пример это ухитрялся ... даже f :: [IORef (IO (IO ()))]выглядит странно.)
leftaroundabout
1
@TimoDenk: Объявление a[x]указывает, что выражение a[i]является действительным, когда i >= 0 && i < x. Принимая во внимание, что a[]оставляет размер неуказанным, и, следовательно, идентичен *a: он указывает, что выражение a[i](или эквивалентно *(a + i)) является допустимым для некоторого диапазона i.
Джон Пурди
4
На сегодняшний день это самый простой способ думать о типах Си, спасибо за это
Алекс Озер
4
Мне это нравится! Намного легче рассуждать, чем глупые спирали. (*f[])()это тип, который вы можете индексировать, затем разыменовывать, затем вызывать, так что это массив указателей на функции.
Линн
32

Так что это «чтение по спирали» является чем-то действительным?

Применение спирального правила или использование cdecl не всегда допустимы. Оба в некоторых случаях терпят неудачу. Спиральное правило работает во многих случаях, но оно не универсально .

Чтобы расшифровать сложные объявления, запомните эти два простых правила:

  • Всегда читайте объявления изнутри : начинайте с самой внутренней, если есть, круглой скобки. Найдите идентификатор, который объявлен, и начните расшифровывать объявление оттуда.

  • Когда есть выбор, всегда отдавайте предпочтение []и ()больше* : если *предшествует идентификатору и []следует за ним, идентификатор представляет собой массив, а не указатель. Аналогично, если *предшествует идентификатору и ()следует за ним, идентификатор представляет функцию, а не указатель. (Круглые скобки всегда можно использовать для переопределения обычного приоритета над []и ()над *.)

Это правило включает зигзаги от одной стороны идентификатора к другой.

Теперь расшифровываем простую декларацию

int *a[10];

Применяя правило:

int *a[10];      "a is"  
     ^  

int *a[10];      "a is an array"  
      ^^^^ 

int *a[10];      "a is an array of pointers"
    ^

int *a[10];      "a is an array of pointers to `int`".  
^^^      

Давайте расшифруем сложное объявление как

void ( *(*f[]) () ) ();  

применяя вышеуказанные правила:

void ( *(*f[]) () ) ();        "f is"  
          ^  

void ( *(*f[]) () ) ();        "f is an array"  
           ^^ 

void ( *(*f[]) () ) ();        "f is an array of pointers" 
         ^    

void ( *(*f[]) () ) ();        "f is an array of pointers to function"   
               ^^     

void ( *(*f[]) () ) ();        "f is an array of pointers to function returning pointer"
       ^   

void ( *(*f[]) () ) ();        "f is an array of pointers to function returning pointer to function" 
                    ^^    

void ( *(*f[]) () ) ();        "f is an array of pointers to function returning pointer to function returning `void`"  
^^^^

Вот GIF, демонстрирующий, как вы идете (нажмите на картинку для увеличения):

введите описание изображения здесь


Упомянутые здесь правила взяты из книги « Программирование на современном подходе» К.Н. Кинга .

haccks
источник
Это похоже на подход стандарта, то есть «декларация отражает использование». Я хотел бы спросить кое-что еще в этом пункте, хотя: Вы предлагаете книгу К. Н. Кинга? Я вижу много хороших отзывов о книге.
Motun
1
Да. Я предлагаю эту книгу. Я начал программировать из этой книги. Хорошие тексты и проблемы там.
хак
Можете ли вы привести пример, когда cdecl не понимает объявление? Я думал, что cdecl использует те же правила синтаксического анализа, что и компиляторы, и, насколько я могу судить, он всегда работает.
Фабио говорит восстановить Монику
@FabioTurati; Функция не может возвращать массивы или функцию. char (x())[5]должно привести к синтаксической ошибке, но cdecl проанализирует ее как: объявить xкак функцию, возвращающую массив 5 ofchar .
хак
12

Это всего лишь «спираль», потому что в этом объявлении только один оператор на каждой стороне в каждом уровне скобок. Утверждение о том, что вы действуете «по спирали», обычно предполагает, что вы должны чередовать массивы и указатели в объявлении, int ***foo[][][]когда в действительности все уровни массива предшествуют любому из уровней указателя.

Random832
источник
Что ж, при «спиральном подходе» вы идете как можно дальше вправо, затем как можно дальше влево и т. Д. Но это часто объясняется ошибочно ...
Линн
7

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

Сергея
источник
3
Тем не менее, важно знать, как его анализировать, даже если только знать, как анализировать typedef!
inetknght
1
@inetknght, способ, которым вы делаете это с typedefs, состоит в том, чтобы сделать их достаточно простыми, чтобы не требовалось никакого анализа.
СергейА
2
Люди, которые задают подобные вопросы во время интервью, делают это только для того, чтобы погладить свое эго.
Кейси
1
@JohnBode, и вы сделаете себе одолжение, набрав возвращаемое значение функции.
SergeyA
1
@JohnBode, я считаю, что вопрос личного выбора не стоит обсуждать. Я вижу ваши предпочтения, у меня все еще есть мои.
СергейА
7

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

Ссылка: Ван дер Линден, 1994 - Страница 76

asamarin
источник
1
Это слово не указывает внутри, как во вложении паренов или в одной строке. Он описывает шаблон «змея» с линией LTR, за которой следует линия RTL.
Potatoswatter
5

Что касается полезности этого, при работе с шеллкодом вы часто видите эту конструкцию:

int (*ret)() = (int(*)())code;
ret();

Хотя этот синтаксис не так сложен, он часто встречается.

Более полный пример в этом вопросе.

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

Casey
источник
5

Декларация

void (*(*f[])())()

это просто неясный способ сказать

Function f[]

с участием

typedef void (*ResultFunction)();

typedef ResultFunction (*Function)();

На практике, вместо ResultFunction и Function потребуется больше описательных имен . Если возможно, я бы также указал списки параметров как void.

Август Карлстрем
источник
4

Я нашел метод, описанный Брюсом Экелом, полезным и простым для понимания:

Определение указателя на функцию

Чтобы определить указатель на функцию, которая не имеет аргументов и не возвращает значения, вы говорите:

void (*funcPtr)();

Когда вы смотрите на такое сложное определение, лучший способ атаковать его - начать с середины и найти выход. «Начать с середины» означает начинать с имени переменной, которая называется funcPtr. «Выходя из положения» означает поиск справа ближайшего предмета (ничего в данном случае; правая скобка не останавливает вас), затем взгляд влево (указатель, обозначенный звездочкой), затем взгляд вправо ( пустой список аргументов, указывающий на функцию, которая не принимает аргументов), затем смотрит влево (void, что указывает на то, что функция не имеет возвращаемого значения). Это движение вправо-влево-вправо работает с большинством объявлений.

Чтобы просмотреть, «начните с середины» («funcPtr - это ...»), перейдите направо (ничего нет - вас остановили правые скобки), перейдите влево и найдите «*» (« ... указатель на ... »), перейдите вправо и найдите пустой список аргументов (« ... функция, не принимающая аргументов ... »), перейдите влево и найдите пустоту (« funcPtr is указатель на функцию, которая не принимает аргументов и возвращает void »).

Вы можете задаться вопросом, почему * funcPtr требует скобок. Если вы их не используете, компилятор увидит:

void *funcPtr();

Вы бы объявили функцию (которая возвращает void *) вместо определения переменной. Вы можете думать о компиляторе как о прохождении того же процесса, что и вы, когда он выясняет, каким должно быть объявление или определение. Ему нужно, чтобы эти круглые скобки «натолкнулись на», поэтому он возвращается влево и находит «*» вместо продолжения вправо и поиска пустого списка аргументов.

Сложные декларации и определения

Кроме того, как только вы поймете, как работает синтаксис объявлений C и C ++, вы можете создавать гораздо более сложные элементы. Например:

//: C03:ComplicatedDefinitions.cpp

/* 1. */     void * (*(*fp1)(int))[10];

/* 2. */     float (*(*fp2)(int,int,float))(int);

/* 3. */     typedef double (*(*(*fp3)())[10])();
             fp3 a;

/* 4. */     int (*(*f4())[10])();


int main() {} ///:~ 

Пройдите через каждый и используйте правую-левую направляющую, чтобы выяснить это. Номер 1 говорит: «fp1 - указатель на функцию, которая принимает целочисленный аргумент и возвращает указатель на массив из 10 пустых указателей».

Номер 2 говорит: «fp2 - это указатель на функцию, которая принимает три аргумента (int, int и float) и возвращает указатель на функцию, которая принимает целочисленный аргумент и возвращает float».

Если вы создаете много сложных определений, вы можете использовать typedef. Номер 3 показывает, как typedef сохраняет ввод сложного описания каждый раз. В нем говорится: «fp3 - это указатель на функцию, которая не принимает аргументов и возвращает указатель на массив из 10 указателей на функции, которые не принимают аргументов и возвращают double». Затем он говорит «а является одним из этих типов fp3». typedef обычно полезен для создания сложных описаний из простых.

Номер 4 - это объявление функции вместо определения переменной. В нем говорится: «f4 - это функция, которая возвращает указатель на массив из 10 указателей на функции, которые возвращают целые числа».

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

Взято из: Мышление в C ++ Том 1, второе издание, глава 3, раздел «Адреса функций» Брюса Эккеля.

user3496846
источник
4

Запомните эти правила для объявлений C,
и приоритет никогда не будет подвергаться сомнению:
начните с суффикса, продолжите с префикса
и прочитайте оба набора изнутри.
- я, середина 1980-х

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

Серьезно, это не трудно научиться делать с первого взгляда; Вы просто должны быть готовы потратить некоторое время на оттачивание навыка. Если вы собираетесь поддерживать или адаптировать C-код, написанный другими людьми, это определенно стоит потратить на это время. Это также забавный трюк для вечеринки, который сводит с ума других программистов, которые этого не изучали.

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

Знаешь, могло быть и хуже. Было юридическое заявление PL / I, которое начиналось с чего-то вроде:

if if if = then then then = else else else = if then ...
keshlam
источник
2
Заявление PL / I было IF IF = THEN THEN THEN = ELSE ELSE ELSE = ENDIF ENDIFи анализируется как if (IF == THEN) then (THEN = ELSE) else (ELSE = ENDIF).
Коул Джонсон
Я думаю, что была версия, которая продвинулась на один шаг дальше, используя условное выражение IF / THEN / ELSE (эквивалент C? :), которое получило третий набор в миксе ... но это было несколько десятилетий и, возможно, зависит от конкретного диалекта языка. Дело в том, что любой язык имеет хотя бы одну патологическую форму.
keshlam
4

Я был первым автором спирального правила, которое я написал очень много лет назад (когда у меня было много волос :), и было честью, когда оно было добавлено в cfaq.

Я написал спиральное правило, чтобы моим студентам и коллегам было легче читать декларации С "в их голове"; т.е. без использования программных инструментов, таких как cdecl.org и т. д. Я никогда не намеревался объявлять, что спиральное правило является каноническим способом синтаксического анализа выражений Си. Тем не менее, я рад видеть, что это правило помогло буквально тысячам студентов и практикующих программистов на C за эти годы!

Для записи,

На многих сайтах, в том числе Линусом Торвальдсом (кем-то, кого я безмерно уважаю), «правильно» неоднократно идентифицировалось, что бывают ситуации, когда мое спиральное правило «нарушается». Наиболее распространенным является:

char *ar[10][10];

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

char *(ar[10][10]);

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

«ar - это двумерный массив указателей на символ 10x10»

Я надеюсь, что спиральное правило будет полезным для изучения C!

PS:

Я люблю образ "С не сложно" :)

Дэвид Андерсон
источник
3
  • недействительным (*(*f[]) ()) ()

Разрешение void>>

  • (*(*f[]) ()) () = пусто

Resoiving ()>>

  • (* (*f[]) ()) = функция, возвращающая (void)

Разрешение *>>

  • (*f[]) () = указатель на (функция, возвращающая (void))

Разрешение ()>>

  • (* f[]) = функция, возвращающая (указатель на (функция, возвращающая (void)))

Разрешение *>>

  • f[] = указатель на (функция, возвращающая (указатель на (функция, возвращающая (void))))

Разрешение [ ]>>

  • f = массив (указатель на (функция, возвращающая (указатель на (функция, возвращающая (void)))))
Shubham
источник