Come implementare effettivamente la regola del cinque?

AGGIORNA in basso

q1: Come implementeresti la regola del cinque per una class che gestisce risorse piuttosto pesanti, ma di cui vuoi che venga valutata in base al valore perché ciò semplifica e abbellisce notevolmente il suo utilizzo? O non sono nemmeno necessari tutti e cinque gli articoli della regola?

In pratica, sto iniziando qualcosa con l’imaging 3D in cui un’immagine è in genere 128 * 128 * 128 doppie. Essere in grado di scrivere cose come questa renderebbe la matematica molto più semplice:

Data a = MakeData(); Data c = 5 * a + ( 1 + MakeMoreData() ) / 3; 

q2: Usando una combinazione di copia elision / RVO / move semantica il compilatore dovrebbe essere in grado di farlo con un minimo di copia, no?

Ho cercato di capire come fare così ho iniziato con le basi; supponiamo che un object implementa il modo tradizionale di implementare copia e assegnazione:

 class AnObject { public: AnObject( size_t n = 0 ) : n( n ), a( new int[ n ] ) {} AnObject( const AnObject& rh ) : n( rh.n ), a( new int[ rh.n ] ) { std::copy( rh.a, rh.a + n, a ); } AnObject& operator = ( AnObject rh ) { swap( *this, rh ); return *this; } friend void swap( AnObject& first, AnObject& second ) { std::swap( first.n, second.n ); std::swap( first.a, second.a ); } ~AnObject() { delete [] a; } private: size_t n; int* a; }; 

Ora inserisci rvalues ​​e sposta la semantica. Per quanto posso dire, sarebbe un’implementazione funzionante:

 AnObject( AnObject&& rh ) : n( rh.n ), a( rh.a ) { rh.n = 0; rh.a = nullptr; } AnObject& operator = ( AnObject&& rh ) { n = rh.n; a = rh.a; rh.n = 0; rh.a = nullptr; return *this; } 

Tuttavia, il compilatore (VC ++ 2010 SP1) non è molto soddisfatto di questo, e i compilatori di solito sono corretti:

 AnObject make() { return AnObject(); } int main() { AnObject a; a = make(); //error C2593: 'operator =' is ambiguous } 

q3: come risolvere questo? Tornando ad AnObject & operator = (const AnObject & rh) certamente lo risolve ma non perdiamo un’opportunità di ottimizzazione piuttosto importante?

A parte questo, è chiaro che il codice per il costruttore e il compito di spostamento è pieno di duplicazione. Quindi per ora ci dimentichiamo dell’ambiguità e proviamo a risolverlo usando la copia e lo swap ma ora per i rvalues. Come spiegato qui non avremmo nemmeno bisogno di uno scambio personalizzato, ma invece std :: swap fa tutto il lavoro, il che sembra molto promettente. Così ho scritto quanto segue, sperando che std :: swap copi il costrutto temporaneo usando il costruttore di mosse, quindi lo cambi con * questo:

 AnObject& operator = ( AnObject&& rh ) { std::swap( *this, rh ); return *this; } 

Ma questo non funziona e porta invece a un overflow dello stack dovuto alla ricorsione infinita dato che std :: swap chiama di nuovo il nostro operatore = (AnObject && rh). q4: qualcuno può fornire un esempio di cosa si intende nell’esempio, quindi?

Possiamo risolvere questo problema fornendo una seconda funzione di scambio:

 AnObject( AnObject&& rh ) { swap( *this, std::move( rh ) ); } AnObject& operator = ( AnObject&& rh ) { swap( *this, std::move( rh ) ); return *this; } friend void swap( AnObject& first, AnObject&& second ) { first.n = second.n; first.a = second.a; second.n = 0; second.a = nullptr; } 

Ora c’è quasi il doppio della quantità di codice, tuttavia la parte di movimento che ne deriva è un movimento abbastanza economico; ma d’altra parte il normale compito non può più beneficiare di copia elision. A questo punto sono molto confuso e non vedo più cosa sia giusto o sbagliato, quindi spero di ottenere un input qui ..

AGGIORNAMENTO Quindi sembra che ci siano due campi:

  • un detto per saltare l’operatore di assegnazione del movimento e continuare a fare ciò che ci ha insegnato C ++ 03, cioè scrivere un singolo operatore di assegnazione che passa l’argomento per valore.
  • l’altro che dice di implementare l’operatore di assegnazione del movimento (dopo tutto, è ora C ++ 11) e l’operatore di assegnazione delle copie prende il suo argomento per riferimento.

(ok e c’è il 3 ° campo che mi dice di usare un vettore, ma questo è un po ‘fuori portata per questa class ipotetica. Ok nella vita reale userei un vettore, e ci sarebbero anche altri membri, ma dal costruttore di movimento / assegnazione non viene generata automaticamente (ancora?) la domanda sarebbe ancora in attesa)

Sfortunatamente non posso testare entrambe le implementazioni in uno scenario reale dal momento che questo progetto è appena iniziato e il modo in cui i dati effettivamente fluiranno non è ancora noto. Quindi ho semplicemente implementato entrambi, aggiunto contatori per l’allocazione, ecc. E ho eseguito un paio di iterazioni di ca. questo codice, dove T è una delle implementazioni:

 template T make() { return T( narraySize ); } template void assign( T& r ) { r = make(); } template void Test() { T a; T b; for( size_t i = 0 ; i < numIter ; ++i ) { assign( a ); assign( b ); T d( a ); T e( b ); T f( make() ); T g( make() + make() ); } } 

O questo codice non è abbastanza buono per testare ciò che cerco, o il compilatore è troppo intelligente: non importa quello che uso per arraySize e numIter, i risultati per entrambi i campi sono praticamente identici: stesso numero di allocazioni, lievi variazioni nei tempi ma nessuna differenza significativa riproducibile.

Quindi, a meno che qualcuno non indichi un modo migliore per testarlo (dato che gli scneario di utilizzo effettivo non sono ancora noti), dovrò concludere che non ha importanza e quindi è lasciato al gusto dello sviluppatore. Nel qual caso selezionerei # 2.