2010-07-12 14 views
25

Secondo Generalized Constant Expressions—Revision 5 quanto segue è illegale.Perché è errato avere funzioni constexpr su più righe?

constexpr int g(int n) // error: body not just ‘‘return expr’’ 
{ 
    int r = n; 
    while (--n > 1) r *= n; 
    return r; 
} 

Questo è perché funzioni tutti 'constexpr' devono essere della forma { return expression; }. Non vedo alcuna ragione per cui debba essere così.

Nella mia mente, l'unica cosa che sarebbe veramente necessaria è che nessuna informazione di stato esterna sia stata letta/scritta e che i parametri che vengono passati siano anche dichiarazioni di "constexpr". Ciò significherebbe che qualsiasi chiamata alla funzione con gli stessi parametri restituirebbe lo stesso risultato, quindi è possibile "sapere" in fase di compilazione.

Il mio problema principale con questo è che sembra proprio che ti costringa a fare davvero rotonde forme di looping e sperare che il compilatore lo ottimizzi in modo che sia altrettanto veloce per le chiamate non constexpr.

Per scrivere una valida constexpr per l'esempio di cui sopra si potrebbe fare:

constexpr int g(int n) // error: body not just ‘‘return expr’’ 
{ 
    return (n <= 1) ? n : (n * g(n-1)); 
} 

Ma questo è molto più difficile da capire e si deve sperare che il compilatore si occupa della ricorsione di coda quando si chiama con parametri che violano i requisiti per const-expr.

+3

Per me quest'ultimo è molto più leggibile ... – Yuji

+0

Vedere anche: http://stackoverflow.com/questions/8308812/why-is-c11-constexpr-so-restrictive –

risposta

17

Il motivo è che il compilatore ha già molto da fare, senza essere anche un interprete a tutti gli effetti, in grado di valutare codice C++ arbitrario.

Se si attaccano a singole espressioni, limitano il numero di casi da considerare in modo drammatico. In poche parole, semplifica molto le cose che non ci sono punti e virgola in particolare.

Ogni volta che viene rilevato uno ;, significa che il compilatore deve occuparsi degli effetti collaterali. Significa che alcuni stati locali sono stati modificati nella dichiarazione precedente, su cui si baserà la seguente affermazione. Significa che il codice che si sta valutando non è più solo una serie di operazioni semplici che prendono come input l'output dell'operazione precedente, ma richiedono anche l'accesso alla memoria, il che è molto più difficile da ragionare.

In poche parole, questo:

7 * 2 + 4 * 3 

è semplice da calcolare. È possibile costruire un albero di sintassi che assomiglia a questo:

+ 
    /\ 
/\ 
* * 
/\ /\ 
7 2 4 3 

e il compilatore può semplicemente attraversare questo albero di eseguire queste operazioni primitive ad ogni nodo, e il nodo principale è implicitamente il valore di ritorno dell'espressione.

Se dovessimo scrivere lo stesso calcolo l'utilizzo di più linee potremmo fare in questo modo:

int i0 = 7; 
int i1 = 2; 
int i2 = 4; 
int i3 = 3; 

int i4 = i0 * i1; 
int i5 = i2 * i3; 
int i6 = i4 + i5; 
return i6; 

che è molto più difficile da interpretare. Dobbiamo gestire le letture e le scritture della memoria e dobbiamo gestire le dichiarazioni di reso. Il nostro albero di sintassi è diventato molto più complesso. Dobbiamo gestire le dichiarazioni variabili. Dobbiamo gestire istruzioni che non hanno alcun valore di ritorno (ad esempio, un ciclo o una scrittura di memoria), ma che semplicemente modificano un po 'di memoria da qualche parte. Quale memoria? Dove? Cosa succede se sovrascrive accidentalmente parte della memoria del compilatore? Cosa succede se segfaults?

Anche senza il brutto 'what-if's, il codice che il compilatore deve interpretare ha appena ottenuto molto più complesso. L'albero di sintassi potrebbe ora essere simile a questa: (LD e ST sono rispettivamente operazioni di magazzino di carico e)

;  
    /\ 
    ST \ 
    /\ \ 
    i0 3 \ 
     ; 
     /\ 
     ST \ 
     /\ \ 
    i1 4 \ 
      ; 
      /\ 
     ST \ 
     /\ \ 
     i2 2 \ 
       ; 
      /\ 
      ST \ 
      /\ \ 
      i3 7 \ 
       ; 
       /\ 
       ST \ 
       /\ \ 
       i4 * \ 
       /\ \ 
       LD LD \ 
       | | \ 
       i0 i1 \ 
         ; 
         /\ 
         ST \ 
         /\ \ 
        i5 * \ 
         /\ \ 
         LD LD \ 
         | | \ 
         i2 i3 \ 
           ; 
           /\ 
          ST \ 
          /\ \ 
          i6 + \ 
           /\ \ 
           LD LD \ 
           | | \ 
           i4 i5 \ 
             LD 
             | 
             i6 

Non solo aspetto molto più complesso, ma anche ora richiede lo stato. Prima, ogni sottostruttura poteva essere interpretata isolatamente. Ora, dipendono tutti dal resto del programma. Una delle operazioni LD leaf non ha senso a meno che non sia posizionata nell'albero in modo che un'operazione ST sia stata eseguita nella stessa posizione in precedenza.

0

Probabilmente è mal formato perché è troppo difficile da implementare. Una decisione analoga è stata presa nella prima versione della norma per quanto riguarda le chiusure delle funzioni dei membri (vale a dire, essere in grado di trasmettere obj.func come funzione richiamabile). Forse una revisione successiva dello standard offrirà più spazio.

+0

Pur vedendo come la funzione deve essere ben formato C++ ad ogni modo, il compilatore potrebbe compilare le funzioni e poi semplicemente eseguirle e usare il valore restituito (non deve collegarsi nell'eseguibile finale, solo un fittizio blocco di memoria insieme alle altre funzioni chiamate da constexpr) –

+1

@Grant : è più facile dirlo che farlo. In genere è piuttosto complicato compilare piccoli snippet di codice C++ in un programma funzionante. Il codice potrebbe dipendere da alcuni stati globali, o una macro, o ... Ci sono molti casi angolari da considerare per farlo in modo efficace – jalf

+0

@Grant: Il problema dell'arresto ti morde sul culo. Inoltre, vedi la mia altra risposta. –

2

Come ho capito, l'hanno mantenuto il più semplice possibile in modo da non complicare la lingua (infatti mi sembra di ricordare un momento in cui le chiamate ricorsive non erano consentite ma non è più il caso). La logica è che è molto più semplice allentare le regole negli standard futuri piuttosto che limitarle.

+1

Questo è un buon punto, vedere come va e aggiungere più funzioni se non è troppo complesso e se non si riscontrano altri problemi dopo il rilascio. Semplicemente fastidioso dover hackerare attorno ad esso. –

5

Nel caso ci sia qualche confusione qui, si è consapevoli che le funzioni/espressioni constexpr sono valutate allo in fase di compilazione. Non ci sono problemi di prestazioni in fase di esecuzione.

Sapendo questo, il motivo che essi permettono solo dichiarazioni singole ritorno in constexpr funzioni è il modo che gli implementatori del compilatore non hanno bisogno di scrivere una macchina virtuale per calcolare il valore costante.

Sono preoccupato per i problemi di QoI con questo però. Mi chiedo se gli implementatori del compilatore saranno abbastanza intelligenti per eseguire la memoizzazione?

constexpr fib(int n) { return < 2 ? 1 : fib(n-1) + fib(n-2); } 

Senza Memoizzazione, la funzione di cui sopra ha O (2 n) complessità, che non è certo qualcosa che mi piacerebbe sentire, anche in fase di compilazione.

+8

La prestazione del runtime è preoccupante se la si chiama con un'espressione non const. Correggetemi se ho torto, ma le funzioni di constexpr possono anche essere chiamate come se fossero una funzione normale. –

+1

@Grant Il punto di Peters Peters è se si guarda alla complessità temporale di questa versione di Fibonacci che i tempi delle build potrebbero esplodere, ma in realtà le funzioni di constexpr avranno lo stesso limite di ricorsione (o similitudine) come modelli. se si chiama una funzione constexpr con argomento che non è un'espressione costante, la funzione verrà valutata in fase di esecuzione anziché in fase di compilazione. Mi stavo solo chiedendo ma penso che i consulenti trarrebbero vantaggio dalla valutazione ponderata di ciò che spinge mpl a fare con i template per ridurre i tempi di build e supportare i programmi pigri (meta) in generale. –

+0

Concordato, considerare 'modello struct Fib {const int value = Fib :: value + Fib :: value; }; '. L'introduzione di 'constexpr' non crea un nuovo desiderio di memoizzazione. – MSalters

2

MODIFICA: Ignora questa risposta. Il documento di riferimento non è aggiornato. Lo standard consentirà una ricorsione limitata (vedi i commenti).

Entrambe le forme sono illegali. La ricorsione non è consentita nelle funzioni di constexpr, a causa della restrizione che una funzione di constexpr non può essere chiamata fino a quando non viene definita. Il collegamento degli OP disponibile afferma esplicitamente:

constexpr int twice(int x); 
enum { bufsz = twice(256) }; // error: twice() isn’t (yet) defined 

constexpr int fac(int x) 
{ return x > 2 ? x * fac(x - 1) : 1; } // error: fac() not defined 
             // before use 

qualche riga più in basso:

il requisito che una funzione costante espressione può chiamare soltanto precedentemente definito funzioni costante espressione ci garantisce don accedere a eventuali problemi relativi alla ricorsione.

...

Noi (ancora) vietare la ricorsione in tutta la sua forma in espressioni costanti.

Senza queste restrizioni si rimane invischiati nel problema dell'arresto (grazie @Grant per fare jogging nella memoria con il commento sull'altra mia risposta). Piuttosto che imporre limiti di ricorsione arbitrari, i progettisti hanno ritenuto più semplice dire semplicemente "No".

+0

Ah, ho perso quel pezzetto. Beh, questo sicuramente diminuisce il mio entusiasmo per constexpr. Speravo di essere in grado di fare algoritmi di hashing per le stringhe, ma vedere come coinvolgono tutti una specie di loop, sembra impossibile per una soluzione generale di compilazione dei tempi. –

+3

"La ricorsione non è consentita nelle funzioni di constexpr". Questo è falso. La carta collegata è del 2007 e nel frattempo a constexpr è stato concesso di essere ricorsivo. Nella bozza di lavoro corrente (n3092) possiamo vedere in "Allegato B (informativo) quantità di implementazione [implimits]" che un compilatore deve consentire almeno 512 richiami di funzioni constrexpr ricorsive annidate. –

+0

D'oh! Ho anche controllato la voce di Wikipedia per confermare e indica anche che la funzione deve essere definita prima che possa essere chiamata, il che significa che non si tratta di ricorsione. –

Problemi correlati