La regla espiral sobre declaraciones: ¿cuándo es un error?

Recientemente aprendí la regla espiral para desofuscar declaraciones complejas, que debe haber sido escrita con una serie de typedefs. Sin embargo, el siguiente comentario me alarma:

Una simplificación frecuentemente citada, que solo funciona para algunos casos simples.

No encuentro void (*signal(int, void (*fp)(int)))(int); un “caso simple”. Lo cual es aún más alarmante, por cierto.

Entonces, mi pregunta es, ¿ en qué situaciones tendré razón para aplicar la regla, y en la que sería un error?

Básicamente, la regla simplemente no funciona, o bien funciona redefiniendo lo que se entiende por espiral (en cuyo caso, no tiene sentido. Considere, por ejemplo:

 int* a[10][15]; 

La regla en espiral daría a es una matriz [10] de puntero a matriz [15] de int, lo cual es incorrecto. En el caso de que el sitio, tampoco funciona; de hecho, en el caso de la signal , ni siquiera está claro dónde comenzar la espiral.

En general, es más fácil encontrar ejemplos de dónde falla la regla que ejemplos donde funciona.

A menudo me siento tentado de decir que analizar una statement de C ++ es simple, pero ningún cuerpo que haya intentado con declaraciones complicadas me creería. Por otro lado, no es tan difícil como a veces se dice. El secreto es pensar en la statement exactamente como lo haría con una expresión, pero con muchos menos operadores y una regla de precedencia muy simple: todos los operadores a la derecha tienen prioridad sobre todos los operadores a la izquierda. En ausencia de paréntesis, esto significa procesar primero todo a la derecha, luego todo a la izquierda, y procesar paréntesis exactamente como lo haría en cualquier otra expresión. La dificultad real no es la syntax per se, sino que da como resultado algunas declaraciones muy complejas y contraintuitivas, en particular cuando están involucrados valores de retorno de función y punteros a funciones: la primera regla derecha, luego la izquierda significa que los operadores en un nivel particular son a menudo ampliamente separados, por ejemplo:

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

El término final en la expansión aquí es int[10] , pero poner el [10] después de la especificación completa de la función es (al menos para mí) muy antinatural, y tengo que parar y resolverlo cada vez. (Es probable que esta tendencia a la dispersión de partes lógicamente adyacentes conduzca a la regla espiral. El problema es, por supuesto, que a falta de paréntesis, no siempre se extienden, cada vez que vea [i][j] , la regla es ir a la derecha, luego ir a la derecha otra vez, en lugar de espiral).

Y dado que ahora estamos pensando en declaraciones en términos de expresiones: ¿qué haces cuando una expresión se vuelve demasiado complicada de leer? Introduce variables intermedias para facilitar la lectura. En el caso de las declaraciones, las “variables intermedias” son typedef . En particular, yo argumentaría que en cualquier momento que parte del tipo de devolución termine después de los argumentos de la función (y muchas otras veces también), debe usar un typedef para hacer la statement más simple. (Esta es una regla de “haz lo que digo, no lo que hago”, sin embargo. Temo que ocasionalmente usaré algunas declaraciones muy complejas).

La regla es correcta Sin embargo, uno debe tener mucho cuidado al aplicarlo.

Sugiero aplicarlo de forma más formal para las declaraciones C99 +.

Lo más importante aquí es reconocer la siguiente estructura recursiva de todas las declaraciones ( const , volatile , static , extern , inline , struct , union , typedef se eliminan de la imagen por simplicidad pero se pueden volver a agregar fácilmente):

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

Sí, eso es todo, cuatro partes.

 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: []'s 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 

Una vez que haya analizado las 4 partes,

[ object ] es [ derived-part2 (containing / returning)] [ derived-part2 (puntero a)] base-type 1 .

Si hay recursión, encuentras tu object (si hay alguno) en la parte inferior de la stack de recursión, será el más interno y obtendrás la statement completa volviendo a subir y recolectando y combinando las partes derivadas en cada nivel de recursividad.

Durante el análisis, puede mover [object] a after [derived-part2] (si corresponde). Esto le dará una statement linealizada y fácil de entender (ver 1 arriba).

Por lo tanto, en

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

usted obtiene:

  1. base-type = char
  2. nivel 1: derived-part1 = * , object = (**(*foo[3][5])(void)) , derived-part2 = [7][9]
  3. nivel 2: derived-part1 = ** , object = (*foo[3][5]) , derived-part2 = (void)
  4. nivel 3: derived-part1 = * , object = foo , derived-part2 = [3][5]

Desde allí:

  1. nivel 3: * [3][5] foo
  2. nivel 2: ** (void) * [3][5] foo
  3. nivel 1: * [7][9] ** (void) * [3][5] foo
  4. finalmente, char * [7][9] ** (void) * [3][5] foo

Ahora, leyendo de derecha a izquierda:

foo es una matriz de 3 matrices de 5 punteros a una función (sin tomar params) que devuelve un puntero a un puntero a una matriz de 7 matrices de 9 punteros a una char.

También puede invertir las dimensiones de la matriz en cada derived-part2 en el proceso.

Esa es tu regla espiral.

Y es fácil ver la espiral. Te sumerges en el [object] cada vez más nested desde la izquierda y luego resurges en la derecha solo para notar que en el nivel superior hay otro par de izquierda y derecha, y así sucesivamente.

P.ej:

 int * a[][5]; 

Esto no es una matriz de punteros a matrices de int .