¿Qué son declaraciones y declaradores y cómo se interpretan sus tipos en el estándar?

¿Cómo define exactamente el estándar que, por ejemplo, float (*(*(&e)[10])())[5] declara una variable de tipo “referencia a una matriz de 10 punteros a la función de () retornando el puntero a la matriz de 5 float “?

Inspirado por la discusión con @DanNissenbaum

Me refiero al estándar C ++ 11 en esta publicación

Declaraciones

Las declaraciones del tipo que nos interesa se conocen como declaraciones simples en la gramática de C ++, que son de una de las siguientes dos formas (§7 / 1):

decl-specifier-seq opt init-declarator-list opt ;
attribute-specifier-seq decl-specifier-seq opt init-declarator-list ;

El atributo-especificador-seq es una secuencia de atributos ( [[something]] ) y / o especificadores de alineación ( alignas(something) ). Dado que estos no afectan el tipo de statement, podemos ignorarlos y la segunda de las dos formas anteriores.

Especificadores de statement

De modo que la primera parte de nuestra statement, decl-specifier-seq , está formada por especificadores de statement. Estas incluyen algunas cosas que podemos ignorar, como los especificadores de almacenamiento ( static , extern , etc.), los especificadores de funciones (en inline , etc.), el especificador friend , etc. Sin embargo, el especificador de una statement de interés para nosotros es el especificador de tipo , que puede incluir palabras clave de tipo simple ( char , int , unsigned , etc.), nombres de tipos definidos por el usuario, cv-qualifiers ( const o volatile ) y otros que no nos importa

Ejemplo : Entonces, un ejemplo simple de un decl-specifier-seq que es solo una secuencia de especificadores de tipo es const int . Otro podría ser unsigned int volatile .

Puedes pensar “Ah, entonces algo como const volatile int int float const también es un decl-specifier-seq ?” Tendrás razón en que se ajusta a las reglas de la gramática, pero las reglas semánticas no permiten tal decl-specifier-seq . De hecho, solo se permite un especificador de tipo, excepto para ciertas combinaciones (como unsigned con int o const con cualquier cosa excepto a sí mismo) y se requiere al menos un calificador no cv (§7.1.6 / 2-3).

Quick Quiz (puede necesitar referenciar el estándar)

  1. ¿Es const int const una secuencia de especificación de statement válida o no? Si no, ¿está prohibido por las reglas sintácticas o semánticas?

    ¡No válido por reglas semánticas! const no puede combinarse consigo mismo.

  2. ¿Es unsigned const int una secuencia de especificador de statement válida o no? Si no, ¿está prohibido por las reglas sintácticas o semánticas?

    ¡Válido! No importa que el const separe el unsigned de int .

  3. ¿Es auto const una secuencia de especificación de statement válida o no? Si no, ¿está prohibido por las reglas sintácticas o semánticas?

    ¡Válido! auto es un especificador de statement pero cambió la categoría en C ++ 11. Antes era un especificador de almacenamiento (como static ), pero ahora es un especificador de tipo.

  4. ¿Es int * const una secuencia de especificación de statement válida o no? Si no, ¿está prohibido por las reglas sintácticas o semánticas?

    Inválido por reglas sintácticas! Si bien puede ser el tipo completo de una statement, solo int es la secuencia de especificación de la statement. Los especificadores de statement solo proporcionan el tipo de base, y no modificadores compuestos como punteros, referencias, matrices, etc.

Declaradores

La segunda parte de una statement simple es la lista init-declarator . Es una secuencia de declaradores separados por comas, cada uno con un inicializador opcional (§8). Cada declarador introduce una sola variable o función en el progtwig. La forma más simple de statement es solo el nombre que está presentando: el ID del declarante . La statement int x, y = 5; tiene una secuencia de especificador de statement que es solo int , seguida de dos declaradores, x e y , el segundo de los cuales tiene un inicializador. Sin embargo, ignoraremos los inicializadores para el rest de esta publicación.

Un declarador puede tener una syntax particularmente compleja porque esta es la parte de la statement que le permite especificar si la variable es un puntero, referencia, matriz, puntero de función, etc. Tenga en cuenta que estos son todos parte del declarador y no la statement como un todo. Esta es precisamente la razón por la cual int* x, y; no declara dos punteros – el asterisco * es parte del declarador de x y no es parte del declarador de y . Una regla importante es que cada declarante debe tener exactamente un identificador de declarador , el nombre que declara. El rest de las reglas sobre declaradores válidos se aplican una vez que se determina el tipo de statement (lo abordaremos más adelante).

Ejemplo : Un ejemplo simple de un declarador es *const p , que declara un puntero const a … algo. El tipo al que apunta viene dado por los especificadores de statement en su statement. Un ejemplo más aterrador es el que se da en la pregunta, (*(*(&e)[10])())[5] , que declara una referencia a un conjunto de indicadores de función que devuelven los punteros a … otra vez, el la parte final del tipo la proporcionan los especificadores de la statement.

Es poco probable que te encuentres con tan horribles declaradores, pero a veces aparecen otros similares. Es una habilidad útil poder leer una statement como la de la pregunta y es una habilidad que viene con la práctica. Es útil entender cómo el estándar interpreta el tipo de statement.

Quick Quiz (puede necesitar referenciar el estándar)

  1. Qué partes de int const unsigned* const array[50]; son los especificadores de statement y el declarador?

    Especificadores de statement: int const unsigned
    Declarador: * const array[50]

  2. ¿Qué partes de volatile char (*fp)(float const), &r = c; Cuáles son los especificadores de statement y los declaradores?

    Especificadores de statement: volatile char
    Declarator # 1: (*fp)(float const)
    Declarator # 2: &r

Tipos de statement

Ahora que sabemos que una statement se compone de una secuencia de especificador de declaradores y una lista de declaradores, podemos empezar a pensar cómo se determina el tipo de statement. Por ejemplo, podría ser obvio que int* p; define p para ser un “puntero a int”, pero para otros tipos no es tan obvio.

Una statement con varios declaradores, digamos 2 declaradores, se considera que son dos declaraciones de identificadores particulares. Es decir, int x, *y; es una statement del identificador x , int x , y una statement del identificador y , int *y .

Los tipos se expresan en el estándar como oraciones de estilo inglés (como “puntero a int”). La interpretación del tipo de una statement en esta forma similar al inglés se realiza en dos partes. Primero, se determina el tipo de especificador de statement. En segundo lugar, se aplica un procedimiento recursivo a la statement como un todo.

Tipo de especificadores de statement

El tipo de una secuencia de especificador de statement está determinada por la Tabla 10 de la norma. Enumera los tipos de las secuencias dado que contienen los especificadores correspondientes en cualquier orden. Entonces, por ejemplo, cualquier secuencia que contenga signed y char en cualquier orden, incluyendo char signed , tiene el tipo “char firmado”. Cualquier cv-qualifier que aparece en la secuencia del especificador de statement se agrega al frente del tipo. Entonces, char const signed tiene el tipo “const signed char”. Esto asegura que, independientemente del orden en que coloque los especificadores, el tipo será el mismo.

Quick Quiz (puede necesitar referenciar el estándar)

  1. ¿Cuál es el tipo de la secuencia del especificador de statement int long const unsigned ?

    “const unsigned long int”

  2. ¿Cuál es el tipo de la secuencia especificadora de statement char volatile ?

    “char volátil”

  3. ¿Cuál es el tipo de la secuencia del especificador de statement auto const ?

    ¡Depende! auto se deducirá del inicializador. Si se deduce que es int , por ejemplo, el tipo será “const int”.

Tipo de statement

Ahora que tenemos el tipo de secuencia de especificación de statement, podemos calcular el tipo de una statement completa de un identificador. Esto se hace aplicando un procedimiento recursivo definido en §8.3. Para explicar este procedimiento, usaré un ejemplo en ejecución. Vamos a calcular el tipo de e en float const (*(*(&e)[10])())[5] .

Paso 1 El primer paso es dividir la statement en la forma TD donde T es la secuencia especificadora de statement y D es el declarador. Entonces obtenemos:

 T = float const D = (*(*(&e)[10])())[5] 

El tipo de T es, por supuesto, “const float”, como determinamos en la sección anterior. Luego buscamos la subsección de §8.3 que coincida con la forma actual de D Encontrará que esto es §8.3.4 Arrays, porque establece que se aplica a las declaraciones de la forma TD donde D tiene la forma:

D1 [ expresión constante ] opt -attribute-seq opt

Nuestra D es de hecho de esa forma donde D1 es (*(*(&e)[10])()) .

Ahora imagine una statement T D1 (nos hemos librado de [5] ).

 T D1 = const float (*(*(&e)[10])()) 

Su tipo es “ T “. Esta sección indica que el tipo de nuestro identificador, e , es “ matriz de 5 T “, donde es lo mismo que en el tipo de statement imaginaria. Para resolver el rest del tipo, necesitamos calcular el tipo de T D1 .

¡Esta es la recursión! Recurrimos de forma recursiva al tipo de una parte interna de la statement, quitándola un poco en cada paso.

Paso 2 Entonces, como antes, dividimos nuestra nueva statement en la forma TD :

 T = const float D = (*(*(&e)[10])()) 

Esto coincide con el párrafo §8.3 / 6 donde D es de la forma ( D1 ) . Este caso es simple, el tipo de TD es simplemente el tipo de T D1 :

 T D1 = const float *(*(&e)[10])() 

Paso 3 Vamos a llamar a este TD ahora y dividirlo de nuevo:

 T = const float D = *(*(&e)[10])() 

Esto coincide con §8.3.1 Punteros donde D es de la forma * D1 . Si T D1 tiene el tipo “ T “, entonces TD tiene el tipo “ puntero a T “. Entonces ahora necesitamos el tipo de T D1 :

 T D1 = const float (*(&e)[10])() 

Paso 4 Lo llamamos TD y lo dividimos:

 T = const float D = (*(&e)[10])() 

Esto coincide con §8.3.5 Funciones donde D es de la forma D1 () . Si T D1 tiene el tipo “ T “, entonces TD tiene la función “ de () devolver T “. Entonces ahora necesitamos el tipo de T D1 :

 T D1 = const float (*(&e)[10]) 

Paso 5 Podemos aplicar la misma regla que hicimos para el paso 2, donde el declarador está simplemente entre paréntesis para terminar con:

 T D1 = const float *(&e)[10] 

Paso 6 Por supuesto, lo dividimos:

 T = const float D = *(&e)[10] 

Emparejamos §8.3.1 Punteros nuevamente con D de la forma * D1 . Si T D1 tiene el tipo “ T “, entonces TD tiene el tipo “ puntero a T “. Entonces ahora necesitamos el tipo de T D1 :

 T D1 = const float (&e)[10] 

Paso 7 Dividirlo:

 T = const float D = (&e)[10] 

Emparejamos §8.3.4 Arrays nuevamente, con D de la forma D1 [10] . Si T D1 tiene el tipo “ T “, entonces TD tiene el tipo “ matriz de 10 T “. Entonces, ¿cuál es el tipo de T D1 ?

 T D1 = const float (&e) 

Paso 8 Aplicar el paso entre paréntesis nuevamente:

 T D1 = const float &e 

Paso 9 Dividirlo:

 T = const float D = &e 

Ahora igualamos §8.3.2 Referencias donde D es de la forma & D1 . Si T D1 tiene el tipo “ T “, entonces TD tiene el tipo “ referencia a T “. Entonces, ¿cuál es el tipo de T D1 ?

 T D1 = const float e 

Paso 10 ¡ Bueno, es solo “T” por supuesto! No hay en este nivel. Esto viene dado por la regla de caso base en §8.3 / 5.

¡Y terminamos!

Entonces, si miramos el tipo que determinamos en cada paso, sustituyendo las de cada nivel a continuación, podemos determinar el tipo de e en float const (*(*(&e)[10])())[5] :

  array of 5 T │ └──────────┐  pointer to T │ └────────────────────────┐  function of () returning T | └──────────┐  pointer to T | └───────────┐  array of 10 T | └────────────┐  reference to T | |  T 

Si combinamos todo esto, lo que obtenemos es:

 reference to array of 10 pointer to function of () returning pointer to array of 5 const float 

¡Bonito! Entonces eso muestra cómo el comstackdor deduce el tipo de una statement. Recuerde que esto se aplica a cada statement de un identificador si hay múltiples declaradores. Intenta descifrar estos:

Quick Quiz (puede necesitar referenciar el estándar)

  1. ¿Cuál es el tipo de x en la statement bool **(*x)[123]; ?

    “puntero a array de 123 puntero a puntero a bool”

  2. ¿Cuáles son los tipos de y y z en la statement int const signed *(*y)(int), &z = i; ?

    y es un “puntero a la función de (int) retornando el puntero a const signed int”
    z es una “referencia a const signed int”

Si alguien tiene alguna corrección, ¡házmelo saber!

Esta es la forma en que analizo float const (*(*(&e)[10])())[5] . En primer lugar, identifique el especificador. Aquí el especificador es float const . Ahora, veamos la precedencia. [] = () > * . Los paréntesis se usan para eliminar la ambigüedad de la precedencia. Teniendo en cuenta la precedencia, identifiquemos la variable ID, que es e . Entonces, e es una referencia a una matriz (desde [] > * ) de 10 punteros a funciones (desde () > * ) que no toman ningún argumento y regresan y un puntero a una matriz de 5 const float. Entonces, el especificador es el último y el rest se analiza según la precedencia.