2015-05-26 10 views
6

Questo è un cattivo schema. Copy-and-swap è migliore.Operatore di assegnamento distruggono e rigenerano: cosa succede se sto attento?

foo & operator = (foo const & other) { 
    static_assert (noexcept(new (this) foo()), "Exception safety violation"); 

    this-> ~ foo(); 
    try { 
     new (this) foo(other); 
    } catch (...) { 
     new (this) foo(); // does not throw 
     throw; 
    } 
    return * this; 
} 

Finché foo è not polymorphic, cosa potrebbe andare male? (Tuttavia, si supponga che è una classe base.)

Background: ho a che fare con il tipo di locale-storage cancellazione, e l'alternativa è quella di implementare swap come due assegnazioni hard-coded attraverso uno spazio di archiviazione locale. Gli oggetti nei blocchi di memoria dell'origine e della destinazione sono di tipi diversi e semplicemente non possono scambiarsi tra loro. Copia/sposta la costruzione definita in termini di tale scambio è due volte più complicata per apparentemente nessun guadagno.

+0

'nuova (questa) foo(); // non getta' - come fai a essere sicuro che non getti? È un'altra precondizione che stai richiedendo per il tipo, che copia e scambia non è? –

+0

@MikeSeymour I costruttori predefiniti spesso non lanciano. Per quanto riguarda questa domanda, prendila come una dichiarazione di fatto. 'static_assert (noexcept (new (this) foo())," Exception safety violation ");' se preferisci. – Potatoswatter

+0

Programmazione orientata alla cura? –

risposta

0

Distruggi-e-rigenera ha un comportamento fondamentalmente diverso da copia-e-scambio. Confronta:

  1. Distruggi il vecchio valore. Ora è sparito per sempre.
  2. Prova a creare un nuovo valore.
  3. Rinuncia e crea un valore predefinito se necessario.

copia-e-swap:

  1. cercare di costruire un nuovo valore.
  2. Lasciare e lasciare il vecchio valore se necessario.
  3. Applicare il nuovo valore.

Entrambi hanno i loro meriti, ma il copy-and-swap è onnipresente, quindi i suoi svantaggi sono mossi dal principio della sorpresa minima. Quindi cerchiamo di emulare il suo comportamento:

foo & operator = (foo const & other) { 
    static_assert (std::is_nothrow_move_constructible<foo>::value 
       || std::is_nothrow_default_constructible<foo>::value 
       , "Exception safety violation"); 

    foo next(other); 
    try { 
     this-> ~ foo(); 
     new (this) foo(std::move(next)); 
    } catch (...) { 
     new (this) foo(); 
     throw; 
    } 
    return * this; 
} 

Anche se più complicato, questo è meglio educati che un lancio swap, che potrebbe lasciare un miscuglio di vecchi e nuovi valori dopo un'eccezione.

Nel caso comune in cui il costruttore mossa non buttare (vi siete ricordati di dichiararla noexcept, giusto?), L'algoritmo riduce bene:

foo & operator = (foo const & other) { 
    foo next(other); 
    // The dangerous part is over now. 

    this-> ~ foo(); 
    new (this) foo(std::move(next)); 
    return * this; 
} 
2

Lo standard rende garanzie circa tali vite interrotti nel [basic.life] §3.8/7:

Se, dopo la durata di un oggetto è terminata e prima della memorizzazione che l'oggetto occupata viene riutilizzato o rilasciato, viene creato un nuovo oggetto nella posizione di memoria occupata dall'oggetto originale, un puntatore che punta all'oggetto originale, un riferimento che si riferiva all'oggetto originale, o il nome dell'oggetto originale farà automaticamente riferimento al nuovo oggetto e , una volta che la vita del nuovo oggetto è iniziata, può essere usata per manipolare il nuovo oggetto, se:

- la memoria per r il nuovo oggetto sovrappone esattamente la posizione di memoria che l'oggetto originale occupato, e

- il nuovo oggetto è dello stesso tipo dell'oggetto originale (ignorando i primo livello CV-qualificazioni), e

- il tipo dell'oggetto originale non è const-qualificato e, se un tipo di classe, non contiene alcun membro dati non statico il cui tipo è const-qualificato o un tipo di riferimento e

- l'oggetto originale era un oggetto più derivato (§1.8) di tipo T e il nuovo oggetto è un oggetto più derivato di tipo T (ovvero non sono sottooggetti di classe base).

L'ultimo punto squalifica il mio caso d'uso. Tuttavia, poiché questo è solo per una classe non polimorfica, è altrettanto utile trasformare il distruttore e i costruttori nelle funzioni membro private destroy e init, rispettivamente.

In altre parole, quando destroy-and-rigenerate è legale, si potrebbe anche fare la stessa cosa usando le funzioni membro e non new/delete.

Un "vantaggio" per questo è che smette di sembrare intelligente, quindi nessun passante ignorante vorrebbe copiare il progetto.