Cosa sono le dichiarazioni e i dichiaratori e in che modo i loro tipi sono interpretati dallo standard?

Come esattamente lo standard definisce che, ad esempio, float (*(*(&e)[10])())[5] dichiara una variabile di tipo “riferimento a matrice di 10 puntatore a function of () che restituisce puntatore all’array di 5 float “?

Ispirato alla discussione con @DanNissenbaum

Mi riferisco allo standard C ++ 11 in questo post

dichiarazioni

Le dichiarazioni del tipo di cui ci occupiamo sono note come semplici dichiarazioni nella grammatica di C ++, che sono di una delle seguenti due forms (§7 / 1):

decl-specifier-seq opt init-dichiaratore-lista opt ;
attribute-specifier-seq decl-specifier-seq opt init-dichiaratore-lista ;

L’ attributo-specifier-seq è una sequenza di attributi ( [[something]] ) e / o specificatori di allineamento ( alignas(something) ). Dal momento che questi non influenzano il tipo della dichiarazione, possiamo ignorarli e la seconda delle due forms precedenti.

Specificatori di dichiarazione

Quindi la prima parte della nostra dichiarazione, il decl-specificatore-seq , è composta da specificatori di dichiarazione. Questi includono alcune cose che possiamo ignorare, come gli specificatori di memoria ( static , extern , ecc.), Gli extern funzione ( inline , ecc.), L’identificatore friend e così via. Tuttavia, l’unico identificatore di dichiarazione che ci interessa è lo specificatore di tipo , che può includere parole chiave di tipo semplice ( char , int , unsigned , ecc.), Nomi di tipi definiti dall’utente, qualificatori di cv ( const o volatile ) e altri che non ci interessa.

Esempio : Quindi un semplice esempio di decl-specificatore-seq che è solo una sequenza di specificatori di tipo è const int . Un altro potrebbe essere unsigned int volatile .

Potresti pensare “Oh, quindi qualcosa come const volatile int int float const è anche un decl-specificatore-seq ?” Avresti ragione che si adatta alle regole della grammatica, ma le regole semantiche non consentono un tale decl-specificatore-seq . Solo un identificatore di tipo è consentito, infatti, ad eccezione di alcune combinazioni (come unsigned con int o const con qualsiasi cosa tranne se stesso) e almeno un qualificatore non cv è richiesto (§7.1.6 / 2-3).

Quick Quiz (potrebbe essere necessario fare riferimento allo standard)

  1. const int const è const int const una sequenza di const int const dichiarazione valida o no? In caso contrario, è vietato dalle regole sintattiche o semantiche?

    Invalido da regole semantiche! const non può essere combinato con se stesso.

  2. unsigned const int è unsigned const int una sequenza di identificatori di dichiarazione valida o no? In caso contrario, è vietato dalle regole sintattiche o semantiche?

    Valido! Non importa che la const separi i unsigned da int .

  3. auto const una sequenza di auto const dichiarazione valida o no? In caso contrario, è vietato dalle regole sintattiche o semantiche?

    Valido! auto è un identificatore di dichiarazione ma ha cambiato categoria in C ++ 11. Prima era un identificatore di memoria (come static ), ma ora è un identificatore di tipo.

  4. int * const una sequenza di int * const dichiarazione valida o no? In caso contrario, è vietato dalle regole sintattiche o semantiche?

    Invalido da regole sintattiche! Mentre questo potrebbe benissimo essere il tipo completo di una dichiarazione, solo l’ int è la sequenza identificatore di dichiarazione. Gli specificatori di dichiarazione forniscono solo il tipo di base e non i modificatori composti come puntatori, riferimenti, matrici, ecc.

dichiaratori

La seconda parte di una dichiarazione semplice è la lista init-declarator . È una sequenza di dichiaratori separati da virgole, ciascuno con un inizializzatore opzionale (§8). Ogni dichiaratore introduce una singola variabile o funzione nel programma. La forma più semplice di dichiaratore è solo il nome che stai introducendo – l’ id dichiaratore . La dichiarazione int x, y = 5; ha una sequenza di specificatori di dichiarazione che è solo int , seguita da due dichiaratori, y , il secondo dei quali ha un inizializzatore. Tuttavia, ignoreremo gli inizializzatori per il resto di questo post.

Un dichiaratore può avere una syntax particolarmente complessa perché questa è la parte della dichiarazione che consente di specificare se la variabile è un puntatore, un riferimento, un array, un puntatore di funzione, ecc. Si noti che questi fanno tutti parte del dichiaratore e non della dichiarazione nel complesso. Questo è precisamente il motivo per cui int* x, y; non dichiara due puntatori: l’asterisco * fa parte del dichiaratore di x e non parte del dichiaratore di y . Una regola importante è che ogni dichiaratore deve avere esattamente un dichiaratore-id – il nome che sta dichiarando. Il resto delle regole sui dichiaratori validi vengono applicate una volta determinato il tipo di dichiarazione (ci arriveremo più tardi).

Esempio : un semplice esempio di un dichiaratore è *const p , che dichiara un puntatore const su … qualcosa. Il tipo a cui punta è dato dagli specificatori di dichiarazione nella sua dichiarazione. Un esempio più terrificante è quello fornito nella domanda, (*(*(&e)[10])())[5] , che dichiara un riferimento a una matrice di puntatori di funzione che restituiscono puntatori a … di nuovo, il la parte finale del tipo è in realtà data dagli specificatori di dichiarazione.

È improbabile che si imbattano in tali orribili dichiaranti, ma a volte appaiono simili. È un’abilità utile per essere in grado di leggere una dichiarazione come quella nella domanda ed è un’abilità che deriva dalla pratica. È utile capire come lo standard interpreta il tipo di una dichiarazione.

Quick Quiz (potrebbe essere necessario fare riferimento allo standard)

  1. Quali parti di int const unsigned* const array[50]; sono gli specificatori di dichiarazione e il dichiaratore?

    Specificatori di dichiarazione: int const unsigned
    Dichiaratore: * const array[50]

  2. Quali parti di volatile char (*fp)(float const), &r = c; sono gli specificatori di dichiarazione e i dichiaratori?

    Specificatori di dichiarazione: volatile char
    Dichiaratore n. 1: (*fp)(float const)
    Dichiaratore n. 2: &r

Tipi di dichiarazione

Ora sappiamo che una dichiarazione è composta da una sequenza di identificatori dichiaratori e da una lista di dichiaratori, possiamo cominciare a pensare a come viene determinato il tipo di una dichiarazione. Ad esempio, potrebbe essere ovvio che int* p; definisce p come “puntatore a int”, ma per altri tipi non è così ovvio.

Una dichiarazione con più dichiaranti, diciamo 2 dichiaratori, è considerata come due dichiarazioni di identificatori particolari. Cioè, int x, *y; è una dichiarazione dell’identificatore x , int x e una dichiarazione dell’identificatore y , int *y .

I tipi sono espressi nello standard come frasi di tipo inglese (come “pointer to int”). L’interpretazione del tipo di una dichiarazione in questa forma simile alla lingua inglese viene eseguita in due parti. Innanzitutto, viene determinato il tipo dello specificatore di dichiarazione. In secondo luogo, una procedura ricorsiva viene applicata alla dichiarazione nel suo insieme.

Tipo di identificatore della dichiarazione

Il tipo di una sequenza identificatore di dichiarazione è determinata dalla tabella 10 dello standard. Elenca i tipi delle sequenze dato che contengono gli specificatori corrispondenti in qualsiasi ordine. Quindi, ad esempio, qualsiasi sequenza che contenga caratteri signed e char in qualsiasi ordine, incluso il char signed , ha tipo “signed char”. Qualsiasi qualificatore cv che appare nella sequenza identificatore di dichiarazione viene aggiunto all’inizio del tipo. Quindi char const signed ha char const signed “const signed char”. Ciò garantisce che indipendentemente dall’ordine in cui vengono inseriti gli identificatori, il tipo sarà lo stesso.

Quick Quiz (potrebbe essere necessario fare riferimento allo standard)

  1. Qual è il tipo della sequenza int long const unsigned dichiarazione int long const unsigned ?

    “const unsigned long int”

  2. Qual è il tipo della sequenza char volatile dichiarazione char volatile ?

    “volatile char”

  3. Qual è il tipo della sequenza auto const dichiarazione auto const ?

    Dipende! auto verrà dedotto dall’inizializzatore. Se si deduce che sia int , ad esempio, il tipo sarà “const int”.

Tipo di dichiarazione

Ora che abbiamo il tipo della sequenza identificatore di dichiarazione, possiamo elaborare il tipo di un’intera dichiarazione di un identificatore. Questo viene fatto applicando una procedura ricorsiva definita al §8.3. Per spiegare questa procedura, userò un esempio in esecuzione. Elaboreremo il tipo di e in float const (*(*(&e)[10])())[5] .

Passo 1 Il primo passo è dividere la dichiarazione nel modulo TD dove T è la sequenza identificatore di dichiarazione e D è il dichiaratore. Quindi otteniamo:

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

Il tipo di T è, ovviamente, “const float”, come determinato nella sezione precedente. Quindi cerchiamo la sottosezione di §8.3 che corrisponde alla forma attuale di D Troverete che questo è §8.3.4 Matrici, perché afferma che si applica alle dichiarazioni del modulo TD dove D ha la forma:

D1 [ espressione costante opt ] attributo-specificatore-seq opt

La nostra D è in effetti quella forma in cui D1 è (*(*(&e)[10])()) .

Ora immagina una dichiarazione T D1 (ci siamo sbarazzati del [5] ).

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

Il tipo è “ T “. Questa sezione afferma che il tipo del nostro identificatore, e , è “ array di 5 T “, dove è lo stesso del tipo della dichiarazione immaginaria. Quindi, per capire il resto del tipo, dobbiamo elaborare il tipo di T D1 .

Questa è la ricorsione! Elaboriamo in modo ricorsivo il tipo di parte interna della dichiarazione, togliendone un po ‘ad ogni passo.

Passaggio 2 Quindi, come prima, abbiamo diviso la nostra nuova dichiarazione nel modulo TD :

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

Corrisponde al paragrafo §8.3 / 6 in cui D è del modulo ( D1 ) . Questo caso è semplice, il tipo di TD è semplicemente il tipo di T D1 :

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

Passaggio 3 Chiamiamo ora questo TD e suddividiamolo di nuovo:

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

Corrisponde a §8.3.1 Puntatori in cui D è del modulo * D1 . Se T D1 ha tipo “ T “, quindi TD ha tipo “ puntatore a T “. Quindi ora abbiamo bisogno del tipo di T D1 :

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

Passaggio 4 Lo chiamiamo TD e lo dividiamo:

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

Corrisponde a §8.3.5 Funzioni in cui D è del modulo D1 () . Se T D1 ha tipo “ T “, quindi TD ha tipo “ funzione di () che restituisce T “. Quindi ora abbiamo bisogno del tipo di T D1 :

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

Passaggio 5 Possiamo applicare la stessa regola che abbiamo fatto per il passaggio 2, in cui il dichiaratore è semplicemente tra parentesi per finire con:

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

Passo 6 Naturalmente, lo abbiamo diviso:

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

Corrispondiamo nuovamente ai §8.3.1 Puntatori con D del modulo * D1 . Se T D1 ha tipo “ T “, quindi TD ha tipo “ puntatore a T “. Quindi ora abbiamo bisogno del tipo di T D1 :

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

Passaggio 7 Suddividilo:

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

Corrispondiamo di nuovo alle matrici §8.3.4, con D del modulo D1 [10] . Se T D1 ha tipo “ T “, quindi TD ha tipo “ array di 10 T “. Allora, qual è il tipo di T D1 ?

 T D1 = const float (&e) 

Passaggio 8 Applica nuovamente il passaggio parentesi:

 T D1 = const float &e 

Passaggio 9 Suddividilo:

 T = const float D = &e 

Ora abbiniamo §8.3.2 Riferimenti dove D è del modulo & D1 . Se T D1 ha tipo “ T “, quindi TD ha tipo “ riferimento a T “. Quindi qual è il tipo di T D1 ?

 T D1 = const float e 

Step 10 Beh, è ​​solo “T”, ovviamente! Non ci sono a questo livello. Questo è dato dalla regola del caso base in §8.3 / 5.

E abbiamo finito!

Quindi ora se guardiamo al tipo che abbiamo determinato ad ogni passaggio, sostituendo da ogni livello sottostante, possiamo determinare il tipo di e in 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 

Se combiniamo tutto questo insieme, ciò che otteniamo è:

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

Bello! Questo mostra come il compilatore deduce il tipo di una dichiarazione. Ricorda che questo è applicato a ciascuna dichiarazione di un identificatore se ci sono più dichiaratori. Prova a capire questi:

Quick Quiz (potrebbe essere necessario fare riferimento allo standard)

  1. Qual è il tipo di x nella dichiarazione bool **(*x)[123]; ?

    “puntatore a matrice di 123 puntatore a puntatore a bool”

  2. Quali sono i tipi di y e z nella dichiarazione int const signed *(*y)(int), &z = i; ?

    y è un “puntatore alla funzione di (int) restituire il puntatore a const signed int”
    z è un “riferimento a const signed int”

Se qualcuno ha delle correzioni, per favore fatemelo sapere!

Ecco come analizzo float const (*(*(&e)[10])())[5] . Prima di tutto, identifica lo specificatore. Qui lo specificatore è float const . Ora, diamo un’occhiata alla precedenza. [] = () > * . Le parentesi sono utilizzate per disambiguare la precedenza. Con la precedenza in mente, identifichiamo la variabile ID, che è e . Quindi, e è un riferimento a un array (poiché [] > * ) di 10 puntatori alle funzioni (since () > * ) che non accetta argomenti e restituisce un puntatore a un array di 5 const float. Quindi lo specificatore viene per ultimo e il resto viene analizzato in base alla precedenza.