Uno sguardo alla semantica di move nello standard 11 del C++

U

MOVE SEMANTIC

Che cosa si intende per semantica di move e perchè si dovrebbe averne consapevolezza quando si scrive un programma in C++ moderno?
La semantica di move è un concetto chiave introdotto a livello di linguaggio a partire dallo standard 11 del C++ ed è una semantica formalmente applicabile ad ogni linguaggio imperativo.
All’atto pratico la semantica di move potrebbe essere implementata dal programmatore anche in un linguaggio scarsamente espressivo e privo di supporto agli oggetti come per esempio il C.
A voler estremizzare, per semantica di move,  si intende la capacità di un tipo a trasferire la sua rappresentazione o rep da un’istanza ad un’altra.

Classic copy

Senza essere eccessivamente astratti, diamo un’occhiata al frammento di codice C++ che segue:

std::string a("my beautiful string"); 
std::string b; 
b = a;

Il comportamento atteso in questo esempio è facilmente intuibile: il valore della stringa a è copiato nella stringa b e dopo l’assegnamento di b la stringa a continua ad assumere lo stesso valore che aveva prima.
Questo comportamento è chiamato (surprise!) copy ed è stato per molto tempo l’unico modo a disposizione del programmatore C++ per far interagire oggetti dello stesso tipo.
Diamo un’occhiata alla definizione dell’operatore di assignemt preposto alla copy:

string& operator= (const string& str);

Gli aspetti più importanti di questa firma sono il modificatore const e la singola & a indicare una reference di tipo lvalue.
Questa firma ci dice che il parametro str non sarà modificato dall’operatore di copia e la reference di tipo lvalue indica che str tipicamente sopravviverà per altri scopi dopo essere stato copiato.

lvalue, rvalue and MOVE

Per evitare la copia, nello standard 98 del C++ occorre necessariamente passare ai puntatori espliciti oppure alle lvalue references, o alternativamente, si rende necessario implementare esplicitamente un metodo o una funzione che esegua il codice preposto alla move.

//avoid copy using raw pointers:

std::string a("my beautiful string"); 
std::string *b = &a;

//avoid copy using an lvalue reference, safer

std::string a("my beautiful string"); 
std::string &b = a;

//avoid copy using an user implemented move

std::string a("my beautiful string"); 
std::string a.move(b); //ups.. we don't have such method in std::string..:(

A partire dallo standard 11 del C++ con l’introduzione a livello di linguaggio della semantica di move, il programmatore può esplicitare la volontà di muovere un’istanza di un tipo anzichè limitarsi a copiarla, il codice del primo esempio in questo caso diventerebbe:

std::string a("my beautiful string"); 
std::string b; 
b = std::move(a);

Nell’esempio, la funzione std::moverestituisce un oggetto che, in terminologia C++ si definisce: eligible for moving.
Più formalmente, la funzione std::move, forza l’argomento passato ad una rvalue reference; un rvalue, in letteratura rappresenta un oggetto dal quale non è possibile effettuare la dereferenziazione, in altre parole: non è mai possibile ottenere l’indirizzo di un rvalue.
A differenza di un lvalue, un rvalue non possiede un nome e normalmente è un oggetto che ha una natura temporanea come per esempio un oggetto restituito da una funzione; un rvalue è il candidato ideale per la move, perchè dopo la move stessa, tipicamente un rvalue cesserà semplicemente di esistere e non sarà più accessibile da nessuno.
Ma allora la stringa a dell’esempio precedente? Quella è sicuramente accessibile dopo la move!
Certo che si, ma teniamo bene a mente che la std::move è stata pensata proprio per forzare un lvalue, che per definizione possiede un nome ed è quindi sotto la responsabilità del programmatore, ad essere interpretato come un rvalue.
La std::move ci consente di forzare un’operazione di move nei casi in cui il linguaggio imporrebbe la scelta, naturale, di un’operazione di copy.
Nello standard 11 e successivi del C++, lo standard committee, ha deciso di introdurre nel linguaggio la possibilità di specificare che una funzione, un operatore, o un metodo di un tipo possa accettare una rvalue reference utilizzando il simbolo && prefisso ad un parametro.
Come appare quindi la definizione dell’operatore di assignemt preposto alla move?

string& operator= (string&& str) noexcept;

A differenza dell’operatore di copia, salta all’occhio per l’appunto lo && ad indicare una rvalue reference e l’assenza del modificatore const.
All’atto pratico, nell’ultimo esempio si registra un importante differenza rispetto al classico assegnamento a = b: infatti, utilizzando la funzione std::move applicata alla stringa a, si ottiene che la stringa b, dopo l’assegnamento, assume come prima il valore di a, ma in questo caso la stringa a subisce l’effetto dell’operazione di move.
Quindi quale valore assume la stringa a – che è un lvalue – dopo l’operazione di move?
La risposta non è scontata e dipende largamente dal comportamento che l’implementatore del tipo ha deciso di attribuire alla semantica di move del tipo stesso.
Accedere ad un oggetto lvalue, che ha subito una move, a livello teorico, ricade nel cosidetto undefined behaviour, anche se normalmente si richiede che l’istanza mossa conservi uno stato valido.
È del tutto ragionevole che un’implementazione corretta di una std::string preveda che dopo un’operazione di move la stringa assuma il valore di una stringa vuota, come se la stringa fosse stata dichiarata con il costruttore vuoto.
Quindi rispetto ad un operazione di copy, che come è logico che sia, lascia sempre inalterata l’istanza copiata, un’operazione che coinvolge una move, modifica sempre l’istanza mossa, non ha senso pertanto, utilizzare il modificare const in operazioni che coinvolgono una move, anzi, utilizzarlo produce ovviamente, un codice che è rifiutato dal compilatore.

move efficiency

Rispetto ad un’operazione di copy assignment è del tutto plausibile che una move assignment sia computazionalmente molto meno costosa; infatti, quello che tipicamente accade è che la rep dell’istanza mossa viene trasferita nell’istanza di destinazione, evitando del tutto il costo della copia, che per tipi particolarmente complessi, può benissimo essere un’operazione estremamente onerosa.
Questo aspetto è molto importante, perchè può, in linea di principio dare la possibilità al compilatore di generare codice oggetto che chiama, quando possibile, le versioni overloaded di funzioni, operatori e metodi che accettano tra i loro parametri le rvalue references, avantaggiandosi quindi delle operazioni di move anzichè di quelle classiche di copy.
Questo aspetto è particolarmente valido quando si utilizzano i containers offerti dalla libreria standard; infatti la progettazione degli stessi è stata pensata per sfruttare quando possibile la semantica di move; tipicamente tutti i containers sono stati dotati delle versioni overloaded di metodi che accettano le rvalue references.
Possedere a livello di linguaggio la semantica di move determina anche il notevole effetto di poter definire tipi move-only, impedendo del tutto la possibilità che le istanze possano essere copiate.
Un esempio importantissimo di un tipo move-only arriva direttamente dalla standard library, ovvero: std::unique_ptr.

NEL PROSSIMO ARTICOLO

Vedremo in dettaglio l’utilizzo degli smart pointers  std::unique_ptr e di std::shared_ptre di come possono fare la differenza rispetto ai classici raw pointers.
Stay tuned!

A proposito di me

Giuseppe Baccini

Sin dalla tenera età è un grande appassionato di informatica e tecnologie.
Dopo gli studi universitari pensa che tutti i problemi possano essere risolti con il Java.
Successivamente, quando viene assunto in un'azienda che si occupa di finanza e il trading, tradisce Java e comincia a corteggiare il C++.
Ancora adesso non è convinto che il divorzio con il primo amore sia stata una buona idea: la seconda moglie è indubbiamente molto problematica.
Non esclude che in futuro possa esserci una seconda ex!

I nostri Partner

Gli articoli più letti

Articoli recenti

Commenti recenti