web-dev-qa-db-fra.com

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

J'ai été surpris que cela ne soit pas apparu dans mes résultats de recherche. Je pensais que quelqu'un l'aurait déjà demandé, étant donné l'utilité de la sémantique des déplacements en C++ 11: 

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

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

118
Mehrdad

La réponse de Herb (avant sa modification) donnait un bon exemple d'un type qui ne devrait pas peut être déplacé: std::mutex.

Le type de mutex natif du système d'exploitation (par exemple, pthread_mutex_t sur 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 sur tous les objets mutex initialisés. Si std::mutex contenait un type de mutex de système d'exploitation natif en tant que membre de données et que l'adresse du type natif devait rester fixe (car le système d'exploitation conserve une liste de pointeurs vers ses mutex), alors soit std::mutex devrait stocker le type de mutex natif sur le tas afin restez au même endroit lorsque vous êtes déplacé entre des objets std::mutex ou que le std::mutex ne doit pas être déplacé. Il est impossible de le stocker sur le tas, car un std::mutex a un constructeur constexpr et doit être éligible pour une initialisation constante (c'est-à-dire une initialisation statique). Ainsi, la construction d'un std::mutex global est garantie avant le début de l'exécution du programme. Par conséquent, son constructeur ne peut pas utiliser new. Donc, la seule option qui reste est que std::mutex soit inamovible.

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

Il existe un autre argument pour ne pas déplacer std::mutex, à savoir qu'il serait très difficile de le faire en toute sécurité, car vous devez savoir que personne ne tente de verrouiller le mutex au moment où il est déplacé. Puisque les mutex sont l’un des éléments constitutifs que vous pouvez utiliser pour empêcher les courses de données, il serait regrettable qu’ils ne soient pas en sécurité contre les races elles-mêmes! Avec un std::mutex inamovible, vous savez que le seul moyen de le verrouiller et de le déverrouiller une fois qu'il a été construit est de le verrouiller et de le déverrouiller, ce qui garantit explicitement la sécurité de ces opérations et l'intrusion dans la course des données. Ce même argument s'applique aux objets std::atomic<T>: à 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'appeler compare_exchange_strong sur l'objet 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 constituent des blocs constitutifs de bas niveau de code concurrent sûr et doivent assurer l’attricité de toutes les opérations qu’ils exécutent. Si la valeur de l'objet peut être déplacée vers un nouvel objet à un moment quelconque, vous devez utiliser une variable atomique pour protéger chaque variable atomique afin de savoir s'il est prudent 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 dirais de manière générale que lorsqu'un objet est simplement un morceau de mémoire, et non un type qui agit en tant que détenteur d'une valeur ou d'une abstraction d'une valeur, il n'a pas de sens de le déplacer. Les types fondamentaux tels que int ne peuvent pas bouger: leur déplacement n'est qu'une copie. Vous ne pouvez pas extraire les entrailles d'une int, vous pouvez copier sa valeur puis la définir sur zéro, mais c'est toujours une int avec une valeur, ce ne sont que des octets de mémoire. Mais une int est toujours déplaçable dans les termes linguistiques 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 le morceau de mémoire et que vous ne pouvez pas non plus copier sa valeur, alors il est non 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), il n’est donc pas logique de le déplacer, il n’est pas non plus copiable, donc non déplaçable.

107
Jonathan Wakely

Réponse courte: Si un type est copiable, il devrait également pouvoir être déplacé. Cependant, l'inverse n'est pas vrai: certains types comme std::unique_ptr sont déplaçables, mais cela n'a pas de sens de les copier; ce sont naturellement des types uniquement mobiles.

Réponse légèrement plus longue suit ...

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

  1. Types de type valeur, tels que int ou vector<widget>. Celles-ci représentent des valeurs et devraient naturellement être copiables. En C++ 11, vous devez généralement considérer le déplacement comme une optimisation de la copie. Ainsi, tous les types pouvant être copiés doivent naturellement être déplaçables ... le déplacement est simplement un moyen efficace de réaliser une copie dans le cas souvent courant où vous ne le faites pas. n avez plus besoin de l’objet original et allez le détruire de toute façon.

  2. Les types de référence existant 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. Celles-ci sont normalement conservées par un pointeur ou une référence, souvent un base* ou un base&, et ne fournissent donc pas de construction de copie pour éviter le découpage en tranches; Si vous souhaitez obtenir un autre objet identique à celui existant, vous appelez généralement une fonction virtuelle telle que clone. Celles-ci n'ont pas besoin de construction ni d'affectation de déplacement pour deux raisons: elles ne sont pas copiables et elles ont déjà une opération de "déplacement" naturelle encore plus efficace: vous copiez/déplacez simplement le pointeur sur l'objet et l'objet lui-même ne fonctionne pas avoir à se déplacer vers un nouvel emplacement de mémoire.

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

56
Herb Sutter

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

  • tous les types mutex (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 les types atomic
  • once_flag

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

17
billz

Une autre raison pour laquelle j’ai trouvé: performances . Supposons que vous ayez une classe 'a' qui contient une valeur . Vous voulez générer une interface permettant à un utilisateur de modifier la valeur pendant un temps limité (pour une portée ).

Un moyen d'y parvenir est de retourner un objet 'scope guard' de 'a' qui redéfinit la valeur dans son destructeur, comme suit:

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 pouvais changer le paramètre change_value_guard, je devrais ajouter à son destructeur un 'if' qui vérifierait si le protecteur a été déplacé, ce qui représente un impact supplémentaire sur les performances.

Ouais, bien sûr, il peut probablement être optimisé par n'importe quel optimiseur sensé, mais il n’en reste pas moins que le langage (cela nécessite C++ 17, cependant, pour pouvoir renvoyer un type non déplaçable nécessite une élision de copie garantie) ne nous oblige pas. payer cela si, si nous n'allons pas déplacer la garde de toute façon autre que la renvoyer depuis la fonction de création (principe de non-paiement-pour-quoi-vous-ne-utilisez pas).

0
saarraz1