Perché le espressioni costanti hanno un’esclusione per un comportamento non definito?

Stavo facendo ricerche su cosa è permesso in un’espressione costante di nucleo * , che è trattato nella sezione 5.19 Espressioni costanti, paragrafo 2 della bozza di standard C ++ che dice:

Un’espressione condizionale è un’espressione costante di core a meno che non implichi una delle seguenti espressioni come sottoespressione potenzialmente valutata (3.2), ma sottoespressioni di operazioni logiche AND (5.14), logiche OR (5.15) e condizionali (5.16) non valutate non sono considerati [Nota: un operatore sovraccarico richiama una funzione.-nota finale]:

ed elenca le esclusioni nei proiettili che seguono e include ( sottolineatura mia ):

un’operazione che avrebbe un comportamento indefinito [Nota: includendo, per esempio, overflow di interi con segno (clausola 5), ​​aritmetica di alcuni puntatori (5.7), divisione per zero (5.6) o alcune operazioni di spostamento (5.8) -end note];

Huh ? Perché le espressioni costanti hanno bisogno di questa clausola per coprire un comportamento indefinito ? C’è qualcosa di speciale nelle espressioni costanti che richiede un comportamento indefinito per avere uno speciale ritagliarsi nelle esclusioni?

Avere questa clausola ci dà vantaggi o strumenti che non avremmo senza di essa?

Per riferimento, sembra l’ultima revisione della proposta per le espressioni costanti generalizzate .

La formulazione è in realtà l’argomento del rapporto sui difetti n. 1313 che dice:

I requisiti per le espressioni costanti non al momento, ma dovrebbero, escludere espressioni con comportamento non definito, come l’aritmetica del puntatore quando i puntatori non puntano a elementi della stessa matrice.

La risoluzione è l’attuale formulazione che abbiamo ora, quindi questo era chiaramente inteso, quindi quali strumenti ci offre?

Vediamo cosa succede quando proviamo a creare una variabile constexpr con un’espressione che contiene un comportamento non definito , useremo clang per tutti gli esempi seguenti. Questo codice ( vederlo dal vivo ):

 constexpr int x = std::numeric_limits::max() + 1 ; 

produce il seguente errore:

 error: constexpr variable 'x' must be initialized by a constant expression constexpr int x = std::numeric_limits::max() + 1 ; ^ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ note: value 2147483648 is outside the range of representable values of type 'int' constexpr int x = std::numeric_limits::max() + 1 ; ^ 

Questo codice ( vederlo dal vivo ):

 constexpr int x = 1 << 33 ; // Assuming 32-bit int 

produce questo errore:

 error: constexpr variable 'x' must be initialized by a constant expression constexpr int x = 1 << 33 ; // Assuming 32-bit int ^ ~~~~~~~ note: shift count 33 >= width of type 'int' (32 bits) constexpr int x = 1 << 33 ; // Assuming 32-bit int ^ 

e questo codice che ha un comportamento non definito in una funzione di constexpr:

 constexpr const char *str = "Hello World" ; constexpr char access( int index ) { return str[index] ; } int main() { constexpr char ch = access( 20 ) ; } 

produce questo errore:

 error: constexpr variable 'ch' must be initialized by a constant expression constexpr char ch = access( 20 ) ; ^ ~~~~~~~~~~~~ note: cannot refer to element 20 of array of 12 elements in a constant expression return str[index] ; ^ 

Bene, è utile che il compilatore possa rilevare un comportamento indefinito in constexpr , o almeno ciò che crede sia indefinito . Nota, gcc si comporta allo stesso modo, tranne che nel caso di comportamento non definito con spostamento a destra e a sinistra, in questi casi gcc genererà un avviso ma continua a vedere l'espressione come costante.

Possiamo utilizzare questa funzionalità tramite SFINAE per rilevare se un'espressione di addizione causerebbe un overflow, il seguente esempio forzato è stato ispirato dalla risposta intelligente di dyp qui :

 #include  #include  template  struct addIsDefined { template  static constexpr bool isDefined() { return isDefinedHelper(0) ; } template  static constexpr bool isDefinedHelper(int) { return true ; } template  static constexpr bool isDefinedHelper(...) { return false ; } }; int main() { std::cout << std::boolalpha << addIsDefined::isDefined<10,10>() << std::endl ; std::cout << std::boolalpha << addIsDefined::isDefined::max(),1>() << std::endl ; std::cout << std::boolalpha << addIsDefined::isDefined::max(),std::numeric_limits::max()>() << std::endl ; } 

quale risulta in ( vederlo dal vivo ):

 true false true 

Non è evidente che lo standard richieda questo comportamento ma a quanto pare questo commento di Howard Hinnant indica che è in effetti:

[...] ed è anche constexpr, il che significa che UB viene catturato in fase di compilazione

Aggiornare

In qualche modo mi sono perso Issue 695 Errori di calcolo della compilazione in funzioni di constexpr che ruotano attorno al testo della sezione 5 paragrafo 4 che diceva ( enfatizza il mio futuro ):

Se durante la valutazione di un'espressione, il risultato non è definito matematicamente o non è compreso nell'intervallo di valori rappresentabili per il suo tipo, il comportamento non è definito, a meno che tale espressione non appaia dove è richiesta un'espressione integrale costante (5.19 [expr.const] ), nel qual caso il programma è mal formato .

e continua dicendo:

inteso come una circonlocuzione standardese accettabile per "valutato in fase di compilazione", un concetto che non è definito direttamente dallo Standard. Non è chiaro che questa formulazione copra adeguatamente le funzioni di constexpr.

e una nota successiva dice:

[...] Esiste una tensione tra il voler diagnosticare gli errori in fase di compilazione e non diagnosticare errori che non si verifichino effettivamente in fase di esecuzione. [...] Il consenso del CWG era che un'espressione come 1/0 dovrebbe semplicemente essere considerato non costante; qualsiasi diagnostica risulterebbe dall'uso dell'espressione in un contesto che richiede un'espressione costante.

che se sto leggendo correttamente conferma che l'intenzione era di essere in grado di diagnosticare un comportamento indefinito in fase di compilazione nel contesto che richiede un'espressione costante.

Non possiamo assolutamente dire che questo era l'intento, ma è fortemente suggerito che lo fosse. La differenza nel modo in cui clang e gcc trattano gli spostamenti indefiniti lascia un po 'di spazio al dubbio.

Ho archiviato un rapporto bug di gcc: il comportamento indefinito a destra e a sinistra non è un errore in un constexpr . Anche se sembra che questo sia conforms, rompe SFINAE e possiamo vedere dalla mia risposta a È un'estensione compilatore conforms per trattare le funzioni di libreria standard di non-constexpr come constexpr? tale divergenza nell'attuazione osservabile agli utenti della SFINAE sembra indesiderabile per la commissione.

Quando parliamo di comportamento indefinito , è importante ricordare che lo standard lascia il comportamento indefinito per questi casi. Non impedisce alle implementazioni di rendere più forti le garanzie. Ad esempio, alcune implementazioni potrebbero garantire che l’overflow di interi con segno si sovrapponga, mentre altri potrebbero garantire la saturazione.

Richiedere ai compilatori di elaborare espressioni costanti che comportino comportamenti indefiniti limiterebbe le garanzie che un’implementazione potrebbe apportare, limitandole a produrre un valore senza effetti collaterali (ciò che lo Standard chiama valore indeterminato ). Ciò esclude molte delle garanzie estese trovate nel mondo reale.

Ad esempio, alcune implementazioni o standard companion (cioè POSIX) possono definire il comportamento della divisione integrale per zero per generare un segnale. Questo è un effetto collaterale che andrebbe perso se l’espressione fosse calcasting al momento della compilazione.

Quindi, queste espressioni vengono rifiutate in fase di compilazione per evitare la perdita di effetti collaterali nell’ambiente di esecuzione.

C’è un altro punto per escludere il comportamento non definito dalle espressioni costanti: le espressioni costanti dovrebbero, per definizione, essere valutate dal compilatore in fase di compilazione. Consentire un’espressione costante per invocare un comportamento indefinito consentirebbe al compilatore stesso di mostrare un comportamento indefinito. E un compilatore che formatta il tuo hard-disk perché compili qualche codice malvagio non è qualcosa che vuoi avere.