Quand rendre un type non déplaçable en C ++ 11?

127

J'ai été surpris que cela n'apparaisse pas dans mes résultats de recherche, je pensais que quelqu'un l'aurait déjà demandé, étant donné l'utilité de la sémantique de déplacement en C ++ 11:

Quand dois-je (ou est-ce une bonne idée pour moi de) rendre une classe non déplaçable en C ++ 11?

(Raisons autres que des problèmes de compatibilité avec le code existant, c'est-à-dire.)

user541686
la source
2
boost a toujours une longueur d'avance - "types coûteux à déplacer" ( boost.org/doc/libs/1_48_0/doc/html/container/move_emplace.html )
SChepurin
1
Je pense que c'est une très bonne et utile question ( +1de ma part) avec une réponse très approfondie de Herb (ou de son jumeau, comme il semble ), alors j'en ai fait une entrée de FAQ. Si quelqu'un s'oppose à me cingler dans le salon , cela peut être discuté là-bas.
sbi
1
Les classes mobiles AFAIK peuvent toujours être sujettes au découpage, il est donc logique d'interdire le déplacement (et la copie) pour toutes les classes de base polymorphes (c'est-à-dire toutes les classes de base avec des fonctions virtuelles).
Philipp
1
@Mehrdad: Je dis juste que "T a un constructeur de mouvement" et " T x = std::move(anotherT);être légal" ne sont pas équivalents. Ce dernier est une demande de mouvement qui pourrait se rabattre sur le cteur de copie dans le cas où T n'a pas de cteur de mouvement. Alors, que signifie exactement «mobile»?
sellibitze
1
@Mehrdad: Consultez la section de la bibliothèque standard C ++ sur ce que signifie "MoveConstructible". Certains itérateurs peuvent ne pas avoir de constructeur de déplacement, mais il s'agit toujours de MoveConstructible. Méfiez-vous des différentes définitions des personnes «mobiles» à l'esprit.
sellibitze

Réponses:

110

La réponse de Herb (avant modification) a fait un bon exemple d'un type qui ne doit pas être mobile: std::mutex.

Le type mutex natif du système d'exploitation (par exemple pthread_mutex_tsur les plates-formes POSIX) peut ne pas être "invariant d'emplacement", ce qui signifie que l'adresse de l'objet fait partie de sa valeur. Par exemple, le système d'exploitation peut conserver une liste de pointeurs vers tous les objets mutex initialisés. S'il std::mutexcontenait un type mutex du système d'exploitation natif en tant que membre de données et que l'adresse du type natif doit rester fixe (car le système d'exploitation maintient une liste de pointeurs vers ses mutex), l'un ou l'autre std::mutexdevrait stocker le type mutex natif sur le tas afin qu'il reste à au même endroit lorsqu'il est déplacé entre des std::mutexobjets ou std::mutexne doit pas bouger. Le stocker sur le tas n'est pas possible, car a std::mutexa un constexprconstructeur et doit être éligible pour une initialisation constante (ie initialisation statique) de sorte qu'un globalstd::mutexest garanti pour être construit avant le début de l'exécution du programme, donc son constructeur ne peut pas utiliser new. Donc, la seule option qui reste est std::mutexd'être inamovible.

Le même raisonnement s'applique aux autres types qui contiennent quelque chose qui nécessite une adresse fixe. Si l'adresse de la ressource doit rester fixe, ne la déplacez pas!

Il y a un autre argument pour ne pas bouger, std::mutexc'est qu'il serait très difficile de le faire en toute sécurité, car vous auriez besoin de savoir que personne n'essaye de verrouiller le mutex au moment où il est déplacé. Étant donné que les mutex sont l'un des éléments de base que vous pouvez utiliser pour empêcher les courses de données, il serait malheureux qu'ils ne soient pas en sécurité contre les races elles-mêmes! Avec un immeuble, std::mutexvous savez que la seule chose que tout le monde peut faire une fois qu'il a été construit et avant qu'il ne soit détruit est de le verrouiller et de le déverrouiller, et ces opérations sont explicitement garanties pour être thread-safe et ne pas introduire de courses de données. Ce même argument s'applique aux std::atomic<T>objets: à moins qu'ils ne puissent être déplacés de manière atomique, il ne serait pas possible de les déplacer en toute sécurité, un autre thread pourrait essayer d'appelercompare_exchange_strongsur l'objet juste au moment où il est déplacé. Donc, un autre cas où les types ne devraient pas être déplaçables est celui où ils sont des blocs de construction de bas niveau d'un code concurrent sûr et doivent garantir l'atomicité de toutes les opérations sur eux. Si la valeur de l'objet peut être déplacée vers un nouvel objet à tout moment, vous devez utiliser une variable atomique pour protéger chaque variable atomique afin que vous sachiez s'il est sûr de l'utiliser ou si elle a été déplacée ... et une variable atomique à protéger cette variable atomique, et ainsi de suite ...

Je pense que je généraliserais pour dire que lorsqu'un objet n'est qu'un pur morceau de mémoire, pas un type qui agit comme un support pour une valeur ou une abstraction d'une valeur, cela n'a pas de sens de le déplacer. Les types fondamentaux tels que intne peuvent pas bouger: les déplacer n'est qu'une copie. Vous ne pouvez pas extraire les tripes d'un int, vous pouvez copier sa valeur puis la mettre à zéro, mais c'est toujours un intavec une valeur, ce ne sont que des octets de mémoire. Mais un intest toujours mobiledans les termes de la langue car une copie est une opération de déplacement valide. Cependant, pour les types non copiables, si vous ne voulez pas ou ne pouvez pas déplacer la partie de mémoire et que vous ne pouvez pas non plus copier sa valeur, alors elle n'est pas déplaçable. Un mutex ou une variable atomique est un emplacement spécifique de la mémoire (traité avec des propriétés spéciales) donc n'a pas de sens de se déplacer, et n'est pas non plus copiable, donc il n'est pas déplaçable.

Jonathan Wakely
la source
17
+1 un exemple moins exotique de quelque chose qui ne peut pas être déplacé parce qu'il a une adresse spéciale est un nœud dans une structure de graphe orienté.
Potatoswatter
3
Si le mutex est non copiable et non déplaçable, comment puis-je copier ou déplacer un objet contenant un mutex? (Comme une classe thread-safe avec son propre mutex pour la synchronisation ...)
tr3w
4
@ tr3w, vous ne pouvez pas, à moins de créer le mutex sur le tas et de le maintenir via un unique_ptr ou similaire
Jonathan Wakely
2
@ tr3w: Ne déplaceriez-vous pas simplement toute la classe sauf la partie mutex?
user541686
3
@BenVoigt, mais le nouvel objet aura son propre mutex. Je pense qu'il veut dire avoir des opérations de déplacement définies par l'utilisateur qui déplacent tous les membres sauf le membre du mutex. Et si l'ancien objet expire? Son mutex expire avec lui.
Jonathan Wakely
57

Réponse courte: si un type est copiable, il doit également être déplaçable. Cependant, l'inverse n'est pas vrai: certains types comme std::unique_ptrsont déplaçables mais cela n'a pas de sens de les copier; ce sont naturellement des types de mouvement uniquement.

Une réponse légèrement plus longue suit ...

Il existe deux types principaux de types (parmi d'autres plus spécifiques tels que les traits):

  1. Les types de type valeur, tels que intou vector<widget>. Celles-ci représentent des valeurs et devraient naturellement être copiables. En C ++ 11, vous devriez généralement considérer le déplacement comme une optimisation de la copie, et donc tous les types copiables doivent naturellement être déplaçables ... le déplacement est juste un moyen efficace de faire une copie dans le cas souvent courant où vous ne le faites pas. Vous n'avez plus besoin de l'objet original et vous allez le détruire de toute façon.

  2. Types de type référence qui existent dans les hiérarchies d'héritage, tels que les classes de base et les classes avec des fonctions membres virtuelles ou protégées. Ceux-ci sont normalement détenus par un pointeur ou une référence, souvent un base*ou base&, et ne fournissent donc pas de construction de copie pour éviter le découpage; si vous voulez obtenir un autre objet comme un objet existant, vous appelez généralement une fonction virtuelle comme clone. Ceux-ci n'ont pas besoin de construction ou d'affectation de déplacement pour deux raisons: ils ne sont pas copiables, et ils ont déjà une opération de «déplacement» naturelle encore plus efficace - il vous suffit de copier / déplacer le pointeur vers l'objet et l'objet lui-même ne le fait pas doivent passer du tout à un nouvel emplacement de mémoire.

La plupart des types appartiennent à l'une de ces deux catégories, mais il existe également d'autres types de types qui sont également utiles, mais plus rares. En particulier ici, les types qui expriment la propriété unique d'une ressource, tels que std::unique_ptr, sont naturellement des types à déplacement uniquement, car ils ne sont pas de type valeur (cela n'a pas de sens de les copier) mais vous les utilisez directement (pas toujours par pointeur ou référence) et souhaitent ainsi déplacer des objets de ce type d'un endroit à un autre.

Herb Sutter
la source
61
Le vrai Herb Sutter voudrait-il se lever? :)
fredoverflow
6
Oui, je suis passé d'un compte Google OAuth à un autre et je ne peux pas être dérangé de chercher un moyen de fusionner les deux connexions qui me donnent ici. (Encore un autre argument contre OAuth parmi les plus convaincants.) Je n'utiliserai probablement plus l'autre, donc c'est ce que je vais utiliser pour l'instant pour le message SO occasionnel.
Herb Sutter
7
Je pensais que std::mutexc'était immobile, car les mutex POSIX sont utilisés par adresse.
Puppy
9
@SChepurin: En fait, cela s'appelle HerbOverflow, alors.
sbi
26
Cela reçoit beaucoup de votes positifs, personne n'a-t-il remarqué qu'il dit quand un type doit être uniquement en mouvement, ce qui n'est pas la question? :)
Jonathan Wakely
18

En fait, lorsque je cherche autour de moi, j'ai trouvé que certains types de C ++ 11 ne sont pas déplaçables:

  • tous mutextypes ( 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
  • tous atomictypes
  • once_flag

Apparemment, il y a une discussion sur Clang: https://groups.google.com/forum/?fromgroups=#!topic/comp.std.c++/pCO1Qqb3Xa4

billz
la source
1
... les itérateurs ne devraient pas être mobiles?! Quoi ... pourquoi?
user541686
ouais, je pense que iterators / iterator adaptorsdevrait être édité car C ++ 11 a move_iterator?
billz
Ok maintenant je suis juste confus. Parlez-vous des itérateurs qui déplacent leurs cibles , ou du déplacement des itérateurs eux - mêmes ?
user541686
1
Il en est ainsi std::reference_wrapper. Ok, les autres semblent en effet immobiles.
Christian Rau
1
Ceux - ci semblent tomber dans trois catégories: 1. types liés à faible accès concurrentiel niveau (Atomics, mutex), 2. classes de base polymorphes ( ios_base, type_info, facet), 3. trucs bizarre assortiment ( sentry). Les seules classes immuables qu'un programmeur moyen écrira sont probablement dans la deuxième catégorie.
Philipp
0

Une autre raison que j'ai trouvée - la performance. Disons que vous avez une classe «a» qui détient une valeur. Vous souhaitez générer une interface qui permet à un utilisateur de modifier la valeur pendant une durée limitée (pour une étendue).

Un moyen d'y parvenir est de renvoyer un objet 'scope guard' à partir de 'a' qui remet la valeur dans son destructeur, comme ceci:

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);
    }
}

Si je rendais change_value_guard mobile, je devrais ajouter un `` si '' à son destructeur qui vérifierait si la garde a été déplacée - c'est un si supplémentaire et un impact sur les performances.

Oui, bien sûr, il peut probablement être optimisé par n'importe quel optimiseur sensé, mais c'est toujours bien que le langage (cela nécessite C ++ 17 cependant, pour pouvoir retourner un type non mobile nécessite une élision de copie garantie) ne nous oblige pas payer cela si nous n'allons pas déplacer la garde de toute façon autre que de la renvoyer de la fonction de création (le principe de ne pas payer pour ce que vous n'utilisez pas).

saarraz1
la source