Filosofia alla base del comportamento indefinito


59

Le specifiche C \ C ++ escludono un gran numero di comportamenti aperti per i compilatori da implementare a modo loro. Ci sono un certo numero di domande che continuano a essere poste qui sullo stesso e abbiamo alcuni post eccellenti a riguardo:

La mia domanda non riguarda il comportamento indefinito, o è davvero negativo. Conosco i pericoli e la maggior parte delle citazioni comportamentali non definite pertinenti dallo standard, quindi per favore astieniti dal pubblicare risposte su quanto sia brutto. Questa domanda riguarda la filosofia che sta dietro tralasciando così tanti comportamenti aperti per l'implementazione del compilatore.

Ho letto un eccellente post sul blog in cui si afferma che le prestazioni sono il motivo principale. Mi chiedevo se le prestazioni fossero gli unici criteri per consentirlo o ci sono altri fattori che influenzano la decisione di lasciare le cose aperte per l'implementazione del compilatore?

Se hai qualche esempio da citare su come un determinato comportamento indefinito offre spazio sufficiente per l'ottimizzazione del compilatore, ti preghiamo di elencarli. Se conosci altri fattori diversi dalle prestazioni, ti preghiamo di sostenere la tua risposta con dettagli sufficienti.

Se non capisci la domanda o non hai prove / fonti sufficienti per sostenere la tua risposta, ti preghiamo di non pubblicare risposte ampiamente speculative.


7
chi ha mai sentito parlare di un computer deterministico comunque?
sova,

1
come indica l'eccellente risposta di litb programmers.stackexchange.com/a/99741/192238 , il titolo e il corpo di questa domanda sembrano un po 'discordanti: "i comportamenti aperti per i compilatori da implementare a modo loro" sono generalmente definiti come definiti dall'implementazione . certo, l'effettivo UB può essere definito dall'autore dell'implementazione, ma il più delle volte non si preoccupano (e lo ottimizzano completamente, ecc.)
underscore_d

Risposte:


49

In primo luogo, noterò che anche se cito solo "C" qui, lo stesso vale anche per C ++.

Il commento che menzionava Godel era in parte (ma solo in parte) in discussione.

Quando si arriva a questo, un comportamento indefinito negli standard C sta in gran parte solo evidenziando il confine tra ciò che lo standard tenta di definire e ciò che non lo fa.

I teoremi di Godel (ce ne sono due) sostengono sostanzialmente che è impossibile definire un sistema matematico che può essere provato (secondo le sue stesse regole) come completo e coerente. Puoi fare le tue regole in modo che possano essere complete (il caso che ha affrontato erano le regole "normali" per i numeri naturali), oppure puoi rendere possibile dimostrarne la coerenza, ma non puoi avere entrambe.

Nel caso di qualcosa come C, ciò non si applica direttamente - per la maggior parte, la "dimostrabilità" della completezza o coerenza del sistema non è una priorità per la maggior parte dei progettisti del linguaggio. Allo stesso tempo, sì, probabilmente sono stati influenzati (almeno in una certa misura) sapendo che è dimostrabilmente impossibile definire un sistema "perfetto" - uno che è dimostratamente completo e coerente. Sapere che una cosa del genere è impossibile potrebbe aver reso un po 'più facile fare un passo indietro, respirare un po' e decidere i limiti di ciò che avrebbero cercato di definire.

A rischio di essere (ancora una volta) accusato di arroganza, definirei lo standard C come governato (in parte) da due idee di base:

  1. Il linguaggio dovrebbe supportare la maggior varietà possibile di hardware (idealmente, tutto l'hardware "sano" fino a un limite inferiore ragionevole).
  2. La lingua dovrebbe supportare la scrittura della più ampia varietà di software possibile per l'ambiente dato.

Il primo significa che se qualcuno definisce una nuova CPU, dovrebbe essere possibile fornire un'implementazione buona, solida e utilizzabile di C, a condizione che il design cada almeno ragionevolmente vicino ad alcune semplici linee guida - fondamentalmente, se segue qualcosa sull'ordine generale del modello Von Neumann e fornisce almeno una quantità minima ragionevole di memoria, che dovrebbe essere sufficiente per consentire un'implementazione in C. Per un'implementazione "ospitata" (una che gira su un sistema operativo) è necessario supportare alcune nozioni che corrispondono ragionevolmente ai file e avere un set di caratteri con un determinato set minimo di caratteri (sono richiesti 91).

Il secondo significa che dovrebbe essere possibile scrivere codice che manipola direttamente l'hardware, in modo da poter scrivere cose come boot loader, sistemi operativi, software incorporato che funziona senza alcun sistema operativo, ecc. Alla fine ci sono alcuni limiti in questo senso, quindi quasi tutti è probabile che il sistema operativo pratico, il boot loader, ecc. contenga almeno un po 'di codice scritto nel linguaggio assembly. Allo stesso modo, anche un piccolo sistema incorporato probabilmente includerà almeno una sorta di routine di libreria pre-scritte per consentire l'accesso ai dispositivi sul sistema host. Sebbene sia difficile definire un limite preciso, l'intento è che la dipendenza da tale codice sia ridotta al minimo.

Il comportamento indefinito nella lingua è in gran parte guidato dall'intenzione che la lingua supporti queste capacità. Ad esempio, la lingua consente di convertire un numero intero arbitrario in un puntatore e di accedere a qualunque cosa si trovi a quell'indirizzo. Lo standard non tenta di dire cosa accadrà quando lo fai (ad esempio, anche la lettura da alcuni indirizzi può avere effetti visibili esternamente). Allo stesso tempo, non cerca di impedirti di fare queste cose, perché è necessario per alcuni tipi di software che dovresti essere in grado di scrivere in C.

C'è un comportamento indefinito guidato anche da altri elementi di design. Ad esempio, un altro intento di C è supportare la compilazione separata. Ciò significa (ad esempio) che si intende che è possibile "collegare" pezzi insieme utilizzando un linker che segue approssimativamente quello che la maggior parte di noi vede come il solito modello di linker. In particolare, dovrebbe essere possibile combinare moduli compilati separatamente in un programma completo senza conoscenza della semantica della lingua.

Esiste un altro tipo di comportamento indefinito (che è molto più comune in C ++ rispetto a C), che è presente semplicemente a causa dei limiti della tecnologia del compilatore: cose che sostanzialmente sappiamo essere errori e che probabilmente il compilatore dovrebbe diagnosticare come errori, ma dati gli attuali limiti della tecnologia del compilatore, è dubbio che possano essere diagnosticati in ogni circostanza. Molti di questi sono guidati da altri requisiti, come ad esempio la compilazione separata, quindi si tratta in gran parte di bilanciare requisiti contrastanti, nel qual caso il comitato ha generalmente optato per supportare maggiori capacità, anche se ciò significa mancanza di diagnosi di alcuni possibili problemi, piuttosto che limitare le capacità per garantire che tutti i possibili problemi vengano diagnosticati.

Queste differenze di intenti guidano la maggior parte delle differenze tra C e qualcosa come Java o i sistemi basati su CLI di Microsoft. Questi ultimi sono abbastanza esplicitamente limitati a lavorare con un set molto più limitato di hardware o richiedono software per emulare l'hardware più specifico a cui si rivolgono. Intendono anche specificamente impedire qualsiasi manipolazione diretta dell'hardware, richiedendo invece di utilizzare qualcosa come JNI o ​​P / Invoke (e il codice scritto in qualcosa come C) per fare un simile tentativo.

Ritornando ai teoremi di Godel per un momento, possiamo tracciare qualcosa di simile: Java e CLI hanno optato per l'alternativa "internamente coerente", mentre C ha optato per l'alternativa "completa". Naturalmente, questa è un'analogia molto approssimativa - dubito che chiunque di tentare una prova formale sia coerenza interna o la completezza in entrambi i casi. Tuttavia, l'idea generale si adatta abbastanza bene alle scelte che hanno preso.


25
Penso che i teoremi di Godel siano un'aringa rossa. Si occupano di provare un sistema dai propri assiomi, il che non è il caso qui: C non ha bisogno di essere specificato in C. È del tutto possibile avere un linguaggio completamente specificato (si consideri una macchina di Turing).
poolie

9
Mi dispiace, ma temo che tu abbia completamente frainteso i teoremi di Godel. Affrontano l'impossibilità di provare tutte le affermazioni vere in un sistema logico coerente; in termini di calcolo, il teorema di incompletezza è analogo a dire che ci sono problemi che non possono essere risolti da nessun programma - i problemi sono analoghi a dichiarazioni vere, programmi a prove e al modello di calcolo al sistema logico. Non ha alcuna connessione con comportamenti indefiniti. Vedere per una spiegazione dell'analogia qui: scottaaronson.com/blog/?p=710 .
Alex ten Brink,

5
Dovrei notare che una macchina Von Neumann non è necessaria per un'implementazione in C. È perfettamente possibile (e nemmeno molto difficile) sviluppare un'implementazione in C per un'architettura di Harvard (e non sarei sorpreso di vedere molte di queste implementazioni su sistemi embedded)
bdonlan,

1
Sfortunatamente, la moderna filosofia del compilatore C porta UB a un livello completamente nuovo. Anche nei casi in cui un programma è stato preparato per affrontare quasi tutte le plausibili conseguenze "naturali" di una particolare forma di comportamento indefinito, e quelle che non è in grado di affrontare sarebbero almeno riconoscibili (ad esempio, traboccamento di numeri interi intrappolati), la nuova filosofia favorisce aggirando qualsiasi codice che non poteva essere eseguito a meno che non si verificasse UB, trasformando il codice che si sarebbe comportato correttamente su qualsiasi implementazione in codice "più efficiente" ma semplicemente sbagliato.
supercat

20

La logica spiega

I termini comportamento non specificato, comportamento non definito e comportamento definito dall'implementazione vengono utilizzati per classificare il risultato della scrittura di programmi le cui proprietà lo Standard non descrive o non può descrivere completamente. L'obiettivo dell'adozione di questa categorizzazione è consentire una certa varietà tra le implementazioni che consentono alla qualità dell'implementazione di essere una forza attiva sul mercato, nonché consentire alcune estensioni popolari , senza rimuovere la cache di conformità allo Standard. L'Appendice F allo Standard cataloga quei comportamenti che rientrano in una di queste tre categorie.

Il comportamento non specificato dà all'implementatore un certo margine di manovra nella traduzione dei programmi. Questa latitudine non si estende fino a quando non riesce a tradurre il programma.

Un comportamento indefinito conferisce alla licenza dell'attore di non rilevare alcuni errori del programma che sono difficili da diagnosticare. Identifica anche le aree di possibile estensione della lingua conforme: l'implementatore può aumentare la lingua fornendo una definizione del comportamento ufficialmente indefinito.

Il comportamento definito dall'implementazione offre a un implementatore la libertà di scegliere l'approccio appropriato, ma richiede che questa scelta sia spiegata all'utente. I comportamenti designati come definiti dall'implementazione sono generalmente quelli in cui un utente può prendere decisioni di codifica significative basate sulla definizione dell'implementazione. Gli attuatori dovrebbero tenere presente questo criterio quando decidono quanto estesa dovrebbe essere una definizione di implementazione. Come nel caso di comportamenti non specificati, la semplice mancata traduzione della fonte contenente il comportamento definito dall'implementazione non è una risposta adeguata.

Importante è anche il vantaggio per i programmi, non solo il vantaggio per le implementazioni. Un programma che dipende da un comportamento indefinito può comunque essere conforme , se accettato da un'implementazione conforme. L'esistenza di comportamenti indefiniti consente a un programma di utilizzare funzioni non portatili esplicitamente contrassegnate come tali ("comportamento indefinito"), senza diventare non conformi. Le note logiche:

Il codice C può essere non portatile. Sebbene si sia sforzato di offrire ai programmatori l'opportunità di scrivere programmi realmente portatili, il Comitato non ha voluto forzare i programmatori a scrivere in modo portabile, per impedire l'uso di C come `` assemblatore di alto livello '': la capacità di scrivere specifici per la macchina il codice è uno dei punti di forza di C. È questo principio che motiva ampiamente a distinguere tra programma rigorosamente conforme e programma conforme (§1.7).

E a 1.7 nota

La triplice definizione di conformità viene utilizzata per ampliare la popolazione di programmi conformi e distinguere tra programmi conformi utilizzando un'unica implementazione e programmi conformi portatili.

Un programma strettamente conforme è un altro termine per un programma massimamente portatile. L'obiettivo è quello di dare al programmatore la possibilità di combattere per creare potenti programmi C che sono anche altamente portatili, senza sminuire i programmi C perfettamente utili che sembrano non essere portatili. Quindi l'avverbio rigorosamente.

Quindi, questo piccolo programma sporco che funziona perfettamente su GCC è ancora conforme !


15

La cosa della velocità è particolarmente un problema rispetto a C. Se il C ++ facesse alcune cose che potrebbero essere sensate, come inizializzare grandi matrici di tipi primitivi, perderebbe una tonnellata di parametri di riferimento al codice C. Quindi C ++ inizializza i propri tipi di dati, ma lascia i tipi C così come erano.

Altri comportamenti indefiniti riflettono solo la realtà. Un esempio è lo spostamento dei bit con un conteggio maggiore del tipo. Questo in realtà differisce tra le generazioni di hardware della stessa famiglia. Se hai un'app a 16 bit, lo stesso binario esatto darà risultati diversi su un 80286 e un 80386. Quindi lo standard linguistico dice che non lo sappiamo!

Alcune cose sono mantenute così come erano, come l'ordine di valutazione delle sottoespressioni non specificato. Inizialmente si riteneva che ciò aiutasse gli scrittori di compilatori a ottimizzare meglio. Oggi i compilatori sono abbastanza buoni per capirlo comunque, ma il costo di trovare tutti i posti nei compilatori esistenti che sfruttano la libertà è semplicemente troppo alto.


+1 per il secondo paragrafo, che mostra qualcosa che sarebbe stato scomodo da specificare come comportamento definito dall'implementazione.
David Thornley,

3
Il bit shift è solo un esempio dell'accettazione del comportamento del compilatore indefinito e dell'utilizzo delle capacità hardware. Sarebbe banale specificare un risultato C per un po 'di spostamento quando il conteggio è maggiore del tipo, ma costoso da implementare su alcuni hardware.
mattnz,

7

Ad esempio, gli accessi al puntatore devono quasi essere indefiniti e non necessariamente solo per motivi di prestazioni. Ad esempio, su alcuni sistemi, il caricamento di registri specifici con un puntatore genererà un'eccezione hardware. Se SPARC accede a un oggetto di memoria allineato in modo errato, si verificherà un errore del bus, ma su x86 sarebbe "solo" lento. È difficile in realtà specificare il comportamento in quei casi poiché l'hardware sottostante determina ciò che accadrà e C ++ è portatile per così tanti tipi di hardware.

Naturalmente offre anche al compilatore la libertà di utilizzare conoscenze specifiche dell'architettura. Per un esempio di comportamento non specificato, lo spostamento corretto dei valori con segno può essere logico o aritmetico a seconda dell'hardware sottostante, per consentire l'utilizzo dell'operazione di spostamento disponibile e non forzare l'emulazione del software.

Credo anche che renda il lavoro del compilatore-sceneggiatore piuttosto semplice, ma non riesco a ricordare l'esempio proprio ora. Lo aggiungerò se ricordo la situazione.


3
Il linguaggio C avrebbe potuto essere specificato in modo tale da dover sempre utilizzare letture byte per byte su sistemi con restrizioni di allineamento e tale da fornire trappole di eccezione con un comportamento ben definito per accessi a indirizzi non validi. Ma ovviamente tutto ciò sarebbe stato incredibilmente costoso (in termini di dimensioni, complessità e prestazioni del codice) e non avrebbe offerto alcun vantaggio al codice corretto e corretto.
R ..

6

Semplice: velocità e portabilità. Se C ++ ti garantisse un'eccezione quando declassavi un puntatore non valido, non sarebbe portatile per l'hardware incorporato. Se il C ++ garantisse altre cose come le primitive sempre inizializzate, allora sarebbe più lento, e nel tempo di origine del C ++, il rallentamento era una cosa molto, molto brutta.


1
Eh? Che cosa hanno a che fare le eccezioni con l'hardware incorporato?
Mason Wheeler,

2
Le eccezioni possono bloccare il sistema in modi molto dannosi per i sistemi integrati che devono rispondere rapidamente. Ci sono situazioni in cui una lettura falsa è molto meno dannosa di un sistema rallentato.
Ingegnere mondiale

1
@ Mason: perché l'hardware deve catturare l'accesso non valido. È facile per Windows lanciare una violazione di accesso e più difficile per l'hardware incorporato senza sistema operativo fare nulla tranne die.
DeadMG

3
Ricorda inoltre che non tutte le CPU dispongono di una MMU per la protezione dagli accessi non validi nell'hardware. Se inizi a richiedere che la tua lingua controlli tutti gli accessi al puntatore, devi emulare un MMU su CPU senza uno - e quindi OGNI accesso alla memoria diventa estremamente costoso.
soffice

4

C è stato inventato su una macchina con byte a 9 bit e nessuna unità in virgola mobile - supponiamo che avesse imposto che i byte fossero 9 bit, parole 18 bit e che i float dovessero essere implementati usando l'aritmatica pre IEEE754?


5
Sospetto che tu stia pensando a Unix - C era originariamente utilizzato sul PDP-11, che era in realtà standard attuali piuttosto convenzionali. Penso che l'idea di base sia comunque valida.
Jerry Coffin,

@Jerry - sì, hai ragione - sto invecchiando!
Martin Beckett,

Sì, capita al migliore di noi, temo.
Jerry Coffin,

4

Non credo che la prima logica per UB sia stata quella di lasciare spazio al compilatore per l'ottimizzazione, ma solo la possibilità di utilizzare l'implementazione ovvia per gli obiettivi in ​​un momento in cui le architetture avevano più varietà di adesso (ricorda se C è stato progettato su un PDP-11 che ha un'architettura alquanto familiare, la prima porta era per Honeywell 635 che è molto meno familiare - parola indirizzabile, usando parole a 36 bit, byte a 6 o 9 bit, indirizzi a 18 bit ... beh almeno ha usato 2 complemento). Ma se l'ottimizzazione pesante non era un obiettivo, l'implementazione ovvia non include l'aggiunta di controlli di runtime per overflow, il conteggio dei turni sulla dimensione del registro, che alias nelle espressioni che modificano più valori.

Un'altra cosa presa in considerazione è stata la facilità di implementazione. Il compilatore CA all'epoca era composto da più passaggi utilizzando più processi perché non sarebbe stato possibile gestire tutto un processo (il programma sarebbe stato troppo grande). Chiedere un pesante controllo di coerenza era fuori mano, specialmente quando riguardava diverse CU. (A tale scopo è stato utilizzato un altro programma rispetto ai compilatori C, lanugine).


Mi chiedo che cosa abbia spinto la mutevole filosofia di UB da "Permettere ai programmatori di usare comportamenti esposti dalla loro piattaforma" a "Trovare scuse per consentire ai compilatori di implementare comportamenti totalmente stravaganti"? Mi chiedo anche quante ottimizzazioni finiscano per migliorare la dimensione del codice dopo che il codice è stato modificato per funzionare con il nuovo compilatore? Non sarei sorpreso se in molti casi l'unico effetto dell'aggiunta di tali "ottimizzazioni" al compilatore fosse costringere i programmatori a scrivere codice più grande e più lento in modo da evitare che il compilatore lo interrompesse.
supercat

È una deriva in POV. Le persone sono diventate meno consapevoli della macchina su cui viene eseguito il loro programma, si sono preoccupate maggiormente della portabilità, quindi hanno evitato a seconda del comportamento indefinito, non specificato e definito dall'implementazione. C'era la pressione sugli ottimizzatori per ottenere i migliori risultati sul benchmark, e questo significa fare uso di ogni trattamento favorevole lasciato dalle specifiche delle lingue. C'è anche il fatto che Internet - Usenet alla volta, oggi SE - anche gli avvocati linguistici tendono a dare una visione distorta della logica e del comportamento alla base degli scrittori di compilatori.
Programmatore

1
Ciò che trovo curioso sono le affermazioni che ho visto per l'effetto di "C presume che i programmatori non si impegneranno mai in comportamenti indefiniti" - un fatto che storicamente non è stato vero. Un'affermazione corretta sarebbe stata "C presumeva che i programmatori non avrebbero innescato un comportamento non definito dallo standard se non fossero preparati ad affrontare le conseguenze della piattaforma naturale di quel comportamento. Dato che C era progettato come un linguaggio di programmazione dei sistemi, gran parte del suo scopo era permettere ai programmatori di fare cose specifiche del sistema non definite dallo standard linguistico; l'idea che non lo farebbero mai è assurda.
supercat

È positivo per i programmatori compiere ulteriori sforzi per garantire la portabilità nei casi in cui piattaforme diverse farebbero intrinsecamente cose diverse , ma gli autori di compilatori sprecano il tempo di tutti quando eliminano comportamenti che storicamente i programmatori avrebbero potuto ragionevolmente prevedere di essere comuni a tutti i futuri compilatori. Dati i numeri interi ie n, tali che n < INT_BITSe i*(1<<n)non traboccerebbero, considererei i<<=n;più chiaro di i=(unsigned)i << n;; su molte piattaforme sarebbe più veloce e più piccolo di i*=(1<<N);. Cosa si guadagna vietando ai compilatori?
supercat

Mentre penso che sarebbe buono per lo standard consentire trappole per molte cose che chiama UB (es. Overflow di numeri interi), e ci sono buone ragioni per non richiedere che le trappole facciano qualcosa di prevedibile, penso che da ogni punto di vista si possa immaginare lo standard sarebbe migliorato se richiedesse che la maggior parte delle forme di UB debba produrre valore indeterminato o documentare il fatto che si riservano il diritto di fare qualcos'altro, senza che sia assolutamente necessario documentare ciò che potrebbe essere qualcos'altro. I compilatori che hanno reso tutto "UB" sarebbero legali, ma probabilmente
sfavorevoli

3

Uno dei primi casi classici era l'aggiunta di numeri interi firmati. Su alcuni dei processori in uso, ciò causerebbe un errore e su altri continuerebbe semplicemente con un valore (probabilmente il valore modulare appropriato). Specificare entrambi i casi significherebbe che i programmi per macchine con lo stile aritmetico sfavorevole dovrebbero avere un codice aggiuntivo, incluso un ramo condizionale, per qualcosa di simile all'aggiunta di numeri interi.


L'aggiunta di numeri interi è un caso interessante; al di là della possibilità di un comportamento trap che in alcuni casi sarebbe utile ma che in altri casi potrebbe causare l'esecuzione di codice casuale, vi sono situazioni in cui sarebbe ragionevole che un compilatore facesse inferenze basate sul fatto che l'overflow dei numeri interi non è specificato per il wrapping. Ad esempio, un compilatore con int16 bit e turni con segno esteso costosi potrebbe calcolare (uchar1*uchar2) >> 4usando un turno senza segno esteso. Sfortunatamente, alcuni compilatori estendono le inferenze non solo ai risultati, ma agli operandi.
supercat

2

Direi che era meno sulla filosofia che sulla realtà - C è sempre stato un linguaggio multipiattaforma, e lo standard deve riflettere questo e il fatto che al momento della pubblicazione di qualsiasi standard, ci sarà un un gran numero di implementazioni su molti hardware diversi. Uno standard che proibisce il comportamento necessario verrebbe ignorato o produrrebbe un organismo di standard concorrente.


Inizialmente, molti comportamenti venivano lasciati indefiniti per consentire la possibilità che sistemi diversi facessero cose diverse, tra cui innescare una trappola hardware con un gestore che può o non può essere configurabile (e che, se non configurato, può causare comportamenti arbitrariamente imprevedibili). Richiedere che uno spostamento a sinistra di un valore negativo non intrappoli, per esempio, romperebbe qualsiasi codice progettato per un sistema in cui lo faceva e si basava su tale comportamento. In breve, sono stati lasciati indefiniti in modo da non impedire agli implementatori di fornire comportamenti ritenuti utili .
supercat

Sfortunatamente, tuttavia, ciò è stato distorto in modo tale che anche il codice che sa che è in esecuzione su un processore che farebbe qualcosa di utile in un caso particolare non può trarre vantaggio da tale comportamento, perché i compilatori possono usare il fatto che lo standard C non specifica il comportamento (anche se la piattaforma lo farebbe) per applicare riscritture bizarro al codice.
supercat

1

Alcuni comportamenti non possono essere definiti con alcun mezzo ragionevole. Intendo accedere a un puntatore eliminato. L'unico modo per rilevarlo sarebbe vietare il valore del puntatore dopo l'eliminazione (memorizzandone il valore da qualche parte e non consentendo più alcuna funzione di allocazione restituirlo). Non solo una tale memorizzazione sarebbe eccessiva, ma per un programma di lunga durata causerebbe l'esaurimento dei valori dei puntatori consentiti.


oppure potresti allocare tutti i puntatori come weak_ptre annullare tutti i riferimenti a un puntatore che diventa deleted ... oh aspetta, ci stiamo avvicinando alla garbage collection: /
Matthieu M.

boost::weak_ptrL'implementazione è un modello abbastanza buono per cominciare con questo modello di utilizzo. Invece di rintracciare e annullare weak_ptrsesternamente, weak_ptrcontribuisce solo al shared_ptrconteggio debole del conteggio e il conteggio debole è fondamentalmente un conto di rimpiazzo al puntatore stesso. Pertanto, è possibile annullare il shared_ptrsenza doverlo eliminare immediatamente. Non è perfetto (puoi ancora avere un sacco di scaduti weak_ptrche mantengono il sottostante shared_countsenza una buona ragione) ma almeno è veloce ed efficiente.
soffice

0

Ti faccio un esempio in cui non c'è praticamente nessuna scelta sensata se non un comportamento indefinito. In linea di principio, qualsiasi puntatore potrebbe puntare alla memoria contenente qualsiasi variabile, con la piccola eccezione delle variabili locali che il compilatore può conoscere non hanno mai avuto il loro indirizzo preso. Tuttavia, per ottenere prestazioni accettabili su una CPU moderna, un compilatore deve copiare i valori delle variabili nei registri. Operare completamente senza memoria non è un inizio.

Questo in pratica ti dà due scelte:

1) Svuota tutto dai registri prima di qualsiasi accesso tramite un puntatore, nel caso in cui il puntatore punti alla memoria di quella particolare variabile. Quindi caricare nuovamente tutto il necessario nel registro, nel caso in cui i valori siano stati modificati tramite il puntatore.

2) Avere un insieme di regole per quando un puntatore è autorizzato ad alias una variabile e quando il compilatore è autorizzato ad assumere che un puntatore non alias una variabile.

C opta per l'opzione 2, perché 1 sarebbe terribile per le prestazioni. Ma poi, cosa succede se un puntatore alias una variabile in un modo proibito dalle regole C? Poiché l'effetto dipende dal fatto che il compilatore abbia effettivamente archiviato la variabile in un registro, non c'è modo per lo standard C di garantire definitivamente risultati specifici.


Ci sarebbe una differenza semantica tra dire "Un compilatore è autorizzato a comportarsi come se X fosse vero" e dire "Qualsiasi programma in cui X non è vero si impegnerà in un comportamento indefinito", sebbene sfortunatamente gli standard non chiariscano la distinzione. In molte situazioni, incluso l'esempio di aliasing, la precedente istruzione consentirebbe molte ottimizzazioni del compilatore che altrimenti sarebbero impossibili; quest'ultimo consente alcune "ottimizzazioni", ma molte di queste ultime sono cose che i programmatori non vorrebbero.
supercat

Ad esempio, se un codice imposta un fooa 42 e quindi chiama un metodo che utilizza un puntatore modificato illegittimamente per impostare foosu 44, posso vedere il beneficio nel dire che fino alla successiva scrittura "legittima" foo, i tentativi di leggerlo potrebbero legittimamente produce 42 o 44, e un'espressione simile foo+foopotrebbe persino produrre 86, ma vedo molti meno benefici nel consentire al compilatore di fare inferenze estese e persino retroattive, cambiando il comportamento indefinito i cui plausibili comportamenti "naturali" sarebbero stati tutti benigni, in una licenza per generare codice senza senso.
supercat

0

Storicamente, il comportamento indefinito aveva due scopi principali:

  1. Per evitare di richiedere agli autori del compilatore di generare codice per gestire condizioni che non avrebbero mai dovuto verificarsi.

  2. Per consentire la possibilità che, in assenza di codice, la gestione esplicita di tali condizioni, le implementazioni possano avere vari tipi di comportamenti "naturali" che, in alcuni casi, sarebbero utili.

Ad esempio, su alcune piattaforme hardware, il tentativo di sommare due numeri interi con segno positivo la cui somma è troppo grande per rientrare in un numero intero con segno produrrà un numero intero con segno negativo. Su altre implementazioni attiverà una trappola del processore. Affinché lo standard C imponga entrambi i comportamenti, i compilatori per piattaforme il cui comportamento naturale diverso dallo standard dovrebbero generare codice aggiuntivo per produrre il comportamento corretto, codice che può essere più costoso del codice per effettuare l'effettiva aggiunta. Peggio ancora, significherebbe che i programmatori che desideravano il comportamento "naturale" avrebbero dovuto aggiungere ancora più codice extra per ottenerlo (e quel codice extra sarebbe di nuovo più costoso dell'aggiunta).

Sfortunatamente, alcuni autori di compilatori hanno adottato la filosofia secondo cui i compilatori dovrebbero fare di tutto per trovare condizioni che possano evocare un comportamento indefinito e, presumendo che tali situazioni non possano mai verificarsi, trarne ampie deduzioni. Pertanto, su un sistema con 32 bit int, dato codice come:

uint32_t foo(uint16_t q, int *p)
{
  if (q > 46340)
    *p++;
  return q*q;
}

lo standard C consentirebbe al compilatore di dire che se q è 46341 o superiore, l'espressione q * q produrrà un risultato troppo grande per adattarsi a un intcomportamento indefinito, e di conseguenza il compilatore avrebbe il diritto di presumere che non può accadere e quindi non sarebbe necessario aumentare *pse ciò accade. Se il codice chiamante utilizza *pcome indicatore che dovrebbe scartare i risultati del calcolo, l'effetto dell'ottimizzazione potrebbe essere quello di prendere il codice che avrebbe prodotto risultati sensati su sistemi che si comportano in quasi tutti i modi immaginabili con overflow di numeri interi (il trapping può essere brutto, ma almeno sarebbe sensato), e lo trasformò in codice che potrebbe comportarsi in modo insensato.


-6

L'efficienza è la solita scusa, ma qualunque sia la scusa, il comportamento indefinito è una terribile idea di portabilità. In effetti i comportamenti indefiniti diventano ipotesi non verificate, non dichiarate.


7
L'OP ha specificato questo: "La mia domanda non riguarda il comportamento indefinito, o è davvero negativo. Conosco i pericoli e la maggior parte delle citazioni di comportamento indefinito pertinenti dallo standard, quindi per favore astieniti dal pubblicare risposte su quanto sia cattivo ". Sembra che tu non abbia letto la domanda.
Etienne de Martel,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.