Quando un oggetto viene distrutto e un nuovo oggetto viene creato allo stesso indirizzo tramite placement-new, le regole sulla provenienza dei puntatori in C++ stabiliscono che il valore del puntatore originale non punta automaticamente al nuovo oggetto. Il compilatore può assumere che i puntatori di un tipo specifico mantengano la loro identità di oggetto per tutta la durata di vita dell'oggetto, consentendo ottimizzazioni aggressive basate su analisi dell'aliasing basata sul tipo. std::launder crea esplicitamente un puntatore che punta al nuovo oggetto, dicendo effettivamente al compilatore che la memoria ora contiene un oggetto distinto di tipo potenzialmente diverso o qualificazione const/volatile. Senza questa intervenzione, dereferenziare il vecchio puntatore viola le regole di aliasing stretto, risultando in comportamento indefinito anche se l'indirizzo contiene memoria valida.
Considera un motore di elaborazione audio in tempo reale che riutilizza un pool fisso di buffer per minimizzare i cache miss della CPU e evitare la frammentazione dell'heap durante le esibizioni dal vivo.
Soluzione 1: Allocazione heap standard
Il prototipo iniziale allocava nuovi oggetti di frame audio per ogni blocco di elaborazione utilizzando new. Anche se semplice, questo causava interruzioni udibili durante le pause della raccolta dei rifiuti e cache miss quando si accedeva a memoria non contigua, rendendolo inaccettabile per l'audio professionale.
Soluzione 2: Placement-new con puntatori raw
Il team è passato a un array pre-allocato di std::aligned_storage_t e ha utilizzato placement-new per costruire i frame in loco. Tuttavia, hanno semplicemente riutilizzato i valori dei puntatori originali dopo la ricostruzione. In build ottimizzate con Clang, il compilatore assumeva che un puntatore a un membro volume const dal frame precedente rimanesse valido, facendolo riutilizzare valori obsoleti dai registri piuttosto che ricaricare dalla memoria dove il nuovo frame conteneva dati diversi.
Soluzione 3: Implementazione di std::launder
Hanno introdotto std::launder dopo ogni operazione di placement-new per ottenere un puntatore alla nuova durata dell'oggetto. Questo ha costretto il compilatore a riconoscere che la memoria ora conteneva un nuovo oggetto con valori distinti, prevenendo la memorizzazione errata nei registri dei membri const dei frame distrutti.
Questa soluzione ha eliminato i glitch audio mantenendo prestazioni senza allocazione, raggiungendo requisiti di latenza sub-millisecondo.
Può std::launder essere utilizzato per cambiare il tipo di un oggetto attivo senza chiamare il suo distruttore?
No, std::launder non estende o altera le durate degli oggetti. Lo standard richiede esplicitamente che la durata dell'oggetto vecchio sia terminata (distruttore chiamato) e che un nuovo oggetto abbia iniziato la propria durata nella stessa memoria prima che std::launder possa essere applicato. Tentare di launder un puntatore a un oggetto la cui durata non è terminata risulta in comportamento indefinito, poiché la macchina astratta C++ mantiene che l'oggetto originale esista ancora a quell'indirizzo.
std::launder modifica il modello di bit sottostante del puntatore?
No, std::launder produce un valore puntatore che confronta uguale all'indirizzo originale ma porta informazioni di provenienza diverse. Anche se le implementazioni tendono a restituire esattamente lo stesso modello di bit, l'operazione non è semplicemente un cast—informa l'analisi dell'aliasing del compilatore che questo puntatore ora si riferisce a un nuovo oggetto. Questa distinzione diventa critica quando il compilatore esegue ottimizzazione su tutto il programma attraverso le unità di traduzione, tracciando i valori dei puntatori attraverso flussi di controllo complessi.
std::launder è superfluo per i tipi trivialmente distruttibili poiché non hanno distruttori?
Anche per i tipi trivialmente distruttibili, std::launder è richiesto ogni volta che la durata di un oggetto termina e un nuovo oggetto viene creato nella stessa memoria. La durata dell'oggetto termina quando la sua memoria viene riutilizzata, indipendentemente dal fatto che un distruttore venga eseguito. Senza std::launder, il compilatore potrebbe assumere che un membro const dell'oggetto vecchio rimanga immutabile quando viene accesso tramite il vecchio puntatore, anche dopo placement-new di un nuovo oggetto con valori di membri const diversi, portando a bug di ottimizzazione silenziosi.