C ++ non ha una copia integrata sulla semantica di scrittura. È possibile implementare tale semantica con una classe di puntatore intelligente o scegliere un approccio a livello di progettazione che rende inutili le copie, ad es. utilizzando il pattern di decorazione . Tuttavia, potresti scoprire che queste ottimizzazioni non valgono il loro sforzo.
Copia su Scrivi puntatore intelligente
L'idea è che i tuoi membri siano memorizzati in un puntatore intelligente che ne gestisce la proprietà. Quando il puntatore intelligente viene copiato, non copierà l'oggetto gestito. Tuttavia, una volta richiesto l'accesso in scrittura, viene eseguita una copia.
Questo non è del tutto banale da correggere, specialmente se ci possono essere altri indicatori nell'oggetto gestito. Alcune implementazioni di CoW si attivano solo quando una copia desidera scrivere sull'oggetto, altre implementazioni eseguono anche una copia quando il proprietario originale scrive sull'oggetto (condiviso).
Dovrai quindi scrivere il tuo puntatore intelligente che implementa la semantica desiderata. Probabilmente, questo può essere implementato con uno sforzo moderato in cima a std::shared_ptr
.
Uno svantaggio significativo di CoW è che queste copie pigre possono rendere il tuo codice molto più difficile da eseguire il debug. Copiare un oggetto C ++ può avere effetti collaterali osservabili, ma ora può accadere in momenti imprevisti. Inoltre, ogni accesso in scrittura deve prima controllare lo stato del puntatore intelligente e deve eventualmente dereferenziare più puntatori.
Motivo decoratore
Il motivo decoratore può essere utilizzato per sovrapporre parti di un oggetto con un'implementazione diversa. Ad esempio, potresti voler sovrapporre parti di struct { A a; B b; C c; }
. Innanzitutto, dobbiamo definire un'interfaccia in modo da poter combinare i nostri Decoratori:
class Data {
public:
virtual A& get_a() = 0;
virtual B& get_b() = 0;
virtual C& get_c() = 0;
};
Ora possiamo implementare questa interfaccia con una classe di archiviazione di base:
class BaseStorage : public Data {
A a; B b; C c;
public:
BaseStorage(A const& a, B const& b, C const& c) : a(a), b(b), c(c) {}
A& get_a() override { return a; }
B& get_b() override { return b; }
C& get_c() override { return c; }
};
Se vogliamo sovrapporre il valore di A
, possiamo definire una classe che delega tutte le chiamate a un oggetto base, eccetto per le richieste dei dati A
:
class OverlayA : public Data {
Data& base;
A a;
public:
OverlayA(Data& base, A&& a) : base(base), a(std::move(a)) {}
A& get_a() override { return a; }
B& get_b() override { return base.get_b(); }
C& get_c() override { return base.get_c(); }
};
Invece di eseguire una copia di alcuni Data
, possiamo ora sovrapporre la parte di essa che stiamo per cambiare:
Data& orig = ...;
// call a function with a copied A
will_change_a(OverlayA(orig, A(orig.get_a())));
Sfortunatamente, questi decoratori rendono davvero difficile scrivere codice const-correct - potresti voler che Data& base
sia un riferimento const per evitare scritture nello storage di base, ma l'interfaccia può anche descrivere dati sovrapposti dove le scritture sono necessarie Questo potrebbe essere espresso attraverso i modelli.
Se il comportamento di qualsiasi implementazione dell'interfaccia Data
modifica direttamente i campi privati piuttosto che passare attraverso i metodi virtuali, ciò potrebbe non scrivere nei dati sovrapposti. Pertanto, l'interfaccia Data
non dovrebbe contenere comportamenti aggiuntivi. È possibile implementare tale comportamento in una classe separata che include un Data&
.
Questa tecnica richiede di sapere in anticipo quali parti dell'oggetto devono essere sovrapposte ai dati copiati. In quanto tale, è potenzialmente soggetto a errori.
Lo schema decoratore crea un elenco collegato di sovrapposizioni. Se l'elenco aumenta di molti livelli in profondità, l'inseguimento del puntatore potrebbe danneggiare le prestazioni.
Le copie sono buone
In molti casi, la creazione di una copia è preferibile all'intelligenza come CoW. In particolare, se gli oggetti in questione non sono molto grandi e sono banalmente copiabili, fare una copia ogni volta potrebbe rivelarsi più economico rispetto alle alternative. A causa della memorizzazione nella cache, i puntatori di ricerca tendono ad essere costosi rispetto alle operazioni su oggetti contigui. Sia CoW che Decoratori hanno un overhead continuo per lettura, mentre le copie hanno solo un overhead prevedibile per copia.
Se il tuo profilo determina che le copie sono un problema di prestazioni, allora potrebbe essere sensato provare uno di questi approcci o una combinazione di essi (ad esempio, eseguire una copia ma usare CoW per i membri costosi da copiare come i vettori potrebbe essere ragionevole ). Ove possibile, evita la proprietà di costosi per copiare i dati nel tuo oggetto.