Quando rendere un tipo non mobile in C ++ 11?


127

Sono rimasto sorpreso dal fatto che ciò non si sia verificato nei miei risultati di ricerca, ho pensato che qualcuno l'avrebbe mai chiesto prima, data l'utilità della semantica di spostamento in C ++ 11:

Quando devo (o è una buona idea per me) rendere una classe non mobile in C ++ 11?

(Motivi diversi dai problemi di compatibilità con il codice esistente, cioè.)


2
boost è sempre un passo avanti: "costoso spostare i tipi" ( boost.org/doc/libs/1_48_0/doc/html/container/move_emplace.html )
SChepurin

1
Penso che questa sia una domanda molto buona e utile ( +1da parte mia) con una risposta molto approfondita da parte di Herb (o del suo gemello, a quanto pare ), quindi l'ho fatta una domanda frequente. Se qualcuno mi fa un rumore metallico nel lounge , allora questo può essere discusso lì.
sabato

1
Le classi mobili AFAIK possono ancora essere soggette a divisione, quindi ha senso vietare lo spostamento (e la copia) per tutte le classi polimorfiche (cioè tutte le classi base con funzioni virtuali).
Philipp

1
@Mehrdad: sto solo dicendo che "T ha un costruttore di mosse" e " T x = std::move(anotherT);essere legali" non sono equivalenti. Quest'ultima è una richiesta di spostamento che potrebbe ricadere sul ctor di copia nel caso in cui T non abbia ctor di movimento. Quindi, cosa significa esattamente "mobile"?
sellibitze

1
@Mehrdad: controlla la sezione della libreria standard C ++ sul significato di "MoveConstructible". Alcuni iteratori potrebbero non avere un costruttore di mosse, ma sono comunque MoveConstructible. Fai attenzione alle diverse definizioni delle persone "mobili".
sellibitze,

Risposte:


110

La risposta di erbe (prima che venisse modificato) in realtà ha dato un buon esempio di un tipo che non dovrebbe essere mobile: std::mutex.

Il tipo di mutex nativo del sistema operativo (ad esempio pthread_mutex_tsu piattaforme POSIX) potrebbe non essere "invariante di posizione", il che significa che l'indirizzo dell'oggetto fa parte del suo valore. Ad esempio, il sistema operativo potrebbe mantenere un elenco di puntatori a tutti gli oggetti mutex inizializzati. Se std::mutexcontenesse un tipo di mutex del sistema operativo nativo come membro di dati e l'indirizzo del tipo nativo deve rimanere fisso (poiché il sistema operativo mantiene un elenco di puntatori ai suoi mutex), l'uno o l'altro std::mutexdovrebbe archiviare il tipo di mutex nativo sull'heap in modo che rimanga a la stessa posizione quando spostato tra gli std::mutexoggetti o std::mutexnon deve muoversi. Memorizzarlo sull'heap non è possibile, perché a std::mutexha un constexprcostruttore e deve essere idoneo all'inizializzazione costante (ovvero inizializzazione statica) in modo che un globalestd::mutexè garantito per essere costruito prima che inizi l'esecuzione del programma, quindi il suo costruttore non può usarlo new. Quindi l'unica opzione rimasta è quella std::mutexdi essere immobili.

Lo stesso ragionamento si applica ad altri tipi che contengono qualcosa che richiede un indirizzo fisso. Se l'indirizzo della risorsa deve rimanere fisso, non spostarlo!

C'è un altro argomento per non muoversi, std::mutexche è che sarebbe molto difficile farlo in sicurezza, perché dovresti sapere che nessuno sta cercando di bloccare il mutex nel momento in cui viene spostato. Dato che i mutex sono uno dei mattoni che puoi usare per prevenire le gare di dati, sarebbe sfortunato se non fossero al sicuro contro le gare stesse! Con un immobile std::mutexconosci le uniche cose che chiunque può farci una volta che è stato costruito e prima che sia stato distrutto è bloccarlo e sbloccarlo, e quelle operazioni sono esplicitamente garantite come thread-safe e non introdurre gare di dati. Lo stesso argomento si applica agli std::atomic<T>oggetti: a meno che non possano essere spostati atomicamente, non sarebbe possibile spostarli in modo sicuro, un altro thread potrebbe tentare di chiamarecompare_exchange_strongsull'oggetto nel momento in cui viene spostato. Quindi un altro caso in cui i tipi non dovrebbero essere mobili è quello in cui sono elementi costitutivi di basso livello di codice concorrente sicuro e devono garantire l'atomicità di tutte le operazioni su di essi. Se il valore dell'oggetto può essere spostato su un nuovo oggetto in qualsiasi momento, dovrai utilizzare una variabile atomica per proteggere ogni variabile atomica in modo da sapere se è sicuro utilizzarla o è stata spostata ... e una variabile atomica per proteggere quella variabile atomica e così via ...

Penso che generalizzerei per dire che quando un oggetto è solo un puro pezzo di memoria, non un tipo che funge da supporto per un valore o astrazione di un valore, non ha senso spostarlo. Tipi fondamentali come intnon possono muoversi: spostarli è solo una copia. Non puoi strappare le budella da un int, puoi copiarne il valore e quindi impostarlo su zero, ma è ancora un intcon un valore, sono solo byte di memoria. Ma un intè ancora mobilenei termini della lingua perché una copia è un'operazione di spostamento valida. Per i tipi non copiabili, tuttavia, se non si desidera o non è possibile spostare il pezzo di memoria e non è possibile copiarne il valore, non è mobile. Un mutex o una variabile atomica è una posizione specifica della memoria (trattata con proprietà speciali), quindi non ha senso spostarsi e non è nemmeno copiabile, quindi non è mobile.


17
+1 un esempio meno esotico di qualcosa che non può essere spostato perché ha un indirizzo speciale è un nodo in una struttura grafica diretta.
Potatoswatter,

3
Se il mutex non è copiabile e non mobile, come posso copiare o spostare un oggetto che contiene un mutex? (Come una classe thread-safe con il proprio mutex per la sincronizzazione ...)
tr3w

4
@ tr3w, non è possibile, a meno che non si crei il mutex sull'heap e lo si mantenga tramite un unique_ptr o simile
Jonathan Wakely

2
@ tr3w: Non sposteresti l'intera classe tranne la parte mutex?
user541686

3
@BenVoigt, ma il nuovo oggetto avrà il suo mutex. Penso che significhi avere operazioni di spostamento definite dall'utente che spostano tutti i membri tranne il membro mutex. E se il vecchio oggetto scadesse? Il suo mutex scade con esso.
Jonathan Wakely,

57

Risposta breve: se un tipo è copiabile, dovrebbe anche essere mobile. Tuttavia, non è vero il contrario: alcuni tipi come std::unique_ptrsono mobili ma non ha senso copiarli; questi sono naturalmente solo tipi di mosse.

Segue una risposta leggermente più lunga ...

Esistono due tipi principali di tipi (tra l'altro quelli più specifici come i tratti):

  1. Tipi simili a valori, come into vector<widget>. Questi rappresentano valori e dovrebbero essere naturalmente copiabili. In C ++ 11, generalmente dovresti pensare a spostare come un'ottimizzazione della copia, e quindi tutti i tipi di copia dovrebbero essere naturalmente mobili ... lo spostamento è solo un modo efficace di fare una copia nel caso spesso comune che non Non ho più bisogno dell'oggetto originale e lo distruggeremo comunque.

  2. Tipi simili a riferimenti presenti nelle gerarchie di ereditarietà, come classi di base e classi con funzioni membro virtuali o protette. Questi sono normalmente mantenuti da un puntatore o da un riferimento, spesso a base*o base&, quindi non forniscono la costruzione della copia per evitare il taglio; se vuoi ottenere un altro oggetto proprio come uno esistente, di solito chiami una funzione virtuale come clone. Questi non hanno bisogno di spostare la costruzione o l'assegnazione per due motivi: non sono copiabili e hanno già un'operazione di "spostamento" naturale ancora più efficiente: basta copiare / spostare il puntatore sull'oggetto e l'oggetto stesso non lo fa spostare in una nuova posizione di memoria a tutti.

La maggior parte dei tipi rientra in una di queste due categorie, ma ci sono anche altri tipi di tipi che sono utili, solo più rari. In particolare qui, i tipi che esprimono la proprietà unica di una risorsa, come ad esempio std::unique_ptr, sono naturalmente solo movimenti, perché non sono simili al valore (non ha senso copiarli) ma li usi direttamente (non sempre tramite puntatore o riferimento) e quindi desidera spostare oggetti di questo tipo da un posto all'altro.


61
Il vero Herb Sutter potrebbe alzarsi per favore? :)
fredoverflow

6
Sì, sono passato dall'uso di un account Google OAuth a un altro e non posso preoccuparmi di cercare un modo per unire i due accessi che mi danno qui. (Ancora un altro argomento contro OAuth tra quelli molto più avvincenti.) Probabilmente non userò di nuovo l'altro, quindi questo è quello che userò per ora per il post SO occasionale.
Herb Sutter,

7
Ho pensato che std::mutexfosse immobile, poiché i mutex POSIX sono usati per indirizzo.
Cucciolo

9
@SChepurin: In realtà, allora si chiama HerbOverflow.
sabato

26
Questo sta ottenendo molti voti positivi, nessuno ha notato che dice quando un tipo dovrebbe essere solo in movimento, quale non è la domanda? :)
Jonathan Wakely,

18

In realtà, quando cerco in giro, ho trovato che alcuni tipi in C ++ 11 non sono mobili:

  • tutti i mutextipi ( recursive_mutex, timed_mutex, recursive_timed_mutex,
  • condition_variable
  • type_info
  • error_category
  • locale::facet
  • random_device
  • seed_seq
  • ios_base
  • basic_istream<charT,traits>::sentry
  • basic_ostream<charT,traits>::sentry
  • tutti i atomictipi
  • once_flag

Apparentemente c'è una discussione su Clang: https://groups.google.com/forum/?fromgroups=#!topic/comp.std.c++/pCO1Qqb3Xa4


1
... gli iteratori non dovrebbero essere mobili ?! Cosa ... perché?
user541686

sì, penso che iterators / iterator adaptorsdovrebbe essere modificato come C ++ 11 ha move_iterator?
Billz,

Okay ora sono solo confuso. Stai parlando di iteratori che muovono i loro obiettivi o di spostare gli iteratori stessi ?
user541686

1
Così è std::reference_wrapper. Ok, gli altri sembrano davvero non mobili.
Christian Rau,

1
Questi sembrano cadere in tre categorie: 1. tipi di basso livello di concorrenza legati (Atomics, mutex), 2. classi di base polimorfiche ( ios_base, type_info, facet), 3. roba strana assortiti ( sentry). Probabilmente le uniche classi inamovibili che un programmatore medio scriverà sono nella seconda categoria.
Philipp

0

Un'altra ragione per cui ho trovato: le prestazioni. Supponi di avere una classe "a" che contiene un valore. Si desidera produrre un'interfaccia che consenta a un utente di modificare il valore per un tempo limitato (per un ambito).

Un modo per raggiungere questo obiettivo è restituire un oggetto 'scope guard' da 'a' che ripristina il valore nel suo distruttore, in questo modo:

class a 
{ 
    int value = 0;

  public:

    struct change_value_guard 
    { 
        friend a;
      private:
        change_value_guard(a& owner, int value) 
            : owner{ owner } 
        { 
            owner.value = value;
        }
        change_value_guard(change_value_guard&&) = delete;
        change_value_guard(const change_value_guard&) = delete;
      public:
        ~change_value_guard()
        {
            owner.value = 0;
        }
      private:
        a& owner;
    };

    change_value_guard changeValue(int newValue)
    { 
        return{ *this, newValue };
    }
};

int main()
{
    a a;
    {
        auto guard = a.changeValue(2);
    }
}

Se rendessi mobile change_value_guard, dovrei aggiungere un 'if' al suo distruttore che verificherebbe se la protezione è stata spostata - questo è un ulteriore se e un impatto sulle prestazioni.

Sì, certo, probabilmente può essere ottimizzato da qualsiasi ottimizzatore sano di mente, ma è comunque bello che il linguaggio (questo richiede C ++ 17, tuttavia, per essere in grado di restituire un tipo non mobile richiede l'elisione della copia garantita) non ci richiede pagarlo se non avessimo intenzione di spostare la guardia in altro modo se non di restituirla dalla funzione di creazione (il principio "non pagare per ciò che non usi").

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.