243 votes

Avantages du passage par valeur et de std::move par rapport au passage par référence

J'apprends le C++ en ce moment et j'essaie d'éviter de prendre de mauvaises habitudes. D'après ce que j'ai compris, clang-tidy contient de nombreuses "meilleures pratiques" et j'essaie de m'y tenir le mieux possible (même si je ne comprends pas forcément pourquoi ils sont déjà considérés comme bons), mais je ne suis pas sûr de comprendre ce qui est recommandé ici.

J'ai utilisé cette classe du tutoriel :

class Creature
{
private:
    std::string m_name;

public:
    Creature(const std::string &name)
            :  m_name{name}
    {
    }
};

C'est pourquoi clang-tidy m'a suggéré de passer par une valeur au lieu d'une référence et d'utiliser std::move . Si c'est le cas, on me propose de faire name une référence (pour s'assurer qu'il n'est pas copié à chaque fois) et l'avertissement suivant std::move n'aura aucun effet car name est un const Je devrais donc le supprimer.

La seule façon de ne pas recevoir d'avertissement est de supprimer const tout à fait :

Creature(std::string name)
        :  m_name{std::move(name)}
{
}

Ce qui semble logique, puisque le seul avantage de const était d'éviter d'altérer la chaîne de caractères originale (ce qui n'est pas le cas puisque je suis passé par valeur). Mais j'ai lu CPlusPlus.com :

Notez toutefois que, dans la bibliothèque standard, le déplacement implique que l'objet déplacé est laissé dans un état valide mais non spécifié. Cela signifie qu'après une telle opération, la valeur de l'objet déplacé ne doit être détruite ou affectée d'une nouvelle valeur que si l'on y accède autrement, on obtient une valeur non spécifiée.

Imaginez maintenant ce code :

std::string nameString("Alex");
Creature c(nameString);

Parce que nameString est transmis par sa valeur, std::move n'invalidera que les name dans le constructeur et ne pas toucher à la chaîne de caractères originale. Mais quels sont les avantages de cette solution ? Il semble que le contenu ne soit copié qu'une seule fois de toute façon - si je passe par référence quand j'appelle m_name{name} Si je passe par la valeur lorsque je la passe (et qu'elle est ensuite déplacée). Je comprends que c'est mieux que de passer par valeur et de ne pas utiliser std::move (parce qu'il est copié deux fois).

Deux questions donc :

  1. Ai-je bien compris ce qui se passe ici ?
  2. Y a-t-il un avantage à utiliser std::move plutôt que de passer par référence et d'appeler simplement m_name{name} ?

7 votes

Avec passage par référence, Creature c("John"); fait une copie supplémentaire

2 votes

Ce lien pourrait être une lecture intéressante, il couvre le passage std::string_view et SSO.

5 votes

J'ai trouvé clang-tidy est un excellent moyen de m'obséder avec des micro-optimisations inutiles au détriment de la lisibilité. La question qu'il faut se poser ici, avant toute autre chose, est de savoir combien de fois nous en fait appeler le Creature constructeur.

278voto

Vittorio Romeo Points 2559
/* (0) */ 
Creature(const std::string &name) : m_name{name} { }
  • A passé valeur se lie à name , alors est copié en m_name .

  • A passé rvaleur se lie à name , alors est copié en m_name .


/* (1) */ 
Creature(std::string name) : m_name{std::move(name)} { }
  • A passé valeur es copié en name , alors est déplacé en m_name .

  • A passé rvaleur es déplacé en name , alors est déplacé en m_name .


/* (2) */ 
Creature(const std::string &name) : m_name{name} { }
Creature(std::string &&rname) : m_name{std::move(rname)} { }
  • A passé valeur se lie à name , alors est copié en m_name .

  • A passé rvaleur se lie à rname , alors est déplacé en m_name .


Les opérations de déplacement sont généralement plus rapides que les copies, (1) est meilleur que (0) si vous passez beaucoup de temporaires. (2) est optimale en termes de copies/déplacements, mais nécessite la répétition du code.

La répétition du code peut être évitée avec transfert parfait :

/* (3) */
template <typename T,
          std::enable_if_t<
              std::is_convertible_v<std::remove_cvref_t<T>, std::string>, 
          int> = 0
         >
Creature(T&& name) : m_name{std::forward<T>(name)} { }

Vous pouvez éventuellement contraindre T afin de restreindre le domaine des types avec lesquels ce constructeur peut être instancié (comme indiqué ci-dessus). Le C++20 vise à simplifier cela avec Concepts .


En C++17, prvalues sont affectés par élision de la copie garantie qui, le cas échéant, réduira le nombre de copies/déplacements lors de la transmission d'arguments aux fonctions.

0 votes

Pour (1), les cas pr-value et xvalue ne sont pas identiques depuis c++17, non ?

2 votes

Notez que vous ne devez pas besoin la SFINAE pour aller de l'avant dans cette affaire. Il n'est nécessaire que pour désambiguïser. Il est vraisemblablement utile pour les messages d'erreur potentiels lors du passage de mauvais arguments

0 votes

@Oliv Oui. Les valeurs x doivent être déplacées, tandis que les valeurs pr peuvent être déplacées :)

73voto

lubgr Points 29224
  1. Ai-je bien compris ce qui se passe ici ?

Oui.

  1. Y a-t-il un avantage à utiliser std::move plutôt que de passer par référence et d'appeler simplement m_name{name} ?

Une signature de fonction facile à comprendre, sans surcharge supplémentaire. La signature indique immédiatement que l'argument sera copié, ce qui évite à l'appelant de se demander si une fonction de type const std::string& peut être stockée en tant que membre de données et devenir une référence pendante par la suite. Il n'est pas nécessaire de surcharger la fonction std::string&& name y const std::string& afin d'éviter les copies inutiles lorsque les valeurs r sont transmises à la fonction. Passer une valeur l

std::string nameString("Alex");
Creature c(nameString);

à la fonction qui prend son argument en valeur provoque une construction de copie et de déplacement. Le passage d'une valeur r à la même fonction

std::string nameString("Alex");
Creature c(std::move(nameString));

est à l'origine de deux constructions de déménagement. En revanche, lorsque le paramètre de la fonction est const std::string& il y aura toujours une copie, même en passant un argument de type rvalue. C'est clairement un avantage tant que le type d'argument est peu coûteux à déplacer-construire (c'est le cas de std::string ).

Mais il y a un inconvénient à considérer : le raisonnement ne fonctionne pas pour les fonctions qui assignent l'argument de la fonction à une autre variable (au lieu de l'initialiser) :

void setName(std::string name)
{
    m_name = std::move(name);
}

entraînera une désallocation de la ressource qui m_name avant qu'il ne soit réaffecté. Je recommande de lire l'article 41 de Effective Modern C++ et aussi cette question .

0 votes

C'est logique, d'autant que cela rend la lecture de la déclaration plus intuitive. Je ne suis pas sûr de bien saisir la partie de votre réponse concernant la désallocation (et de comprendre le fil de discussion lié), donc juste pour vérifier Si j'utilise move l'espace est désalloué. Si je n'utilise pas move Il n'est désalloué que si l'espace alloué est trop petit pour contenir la nouvelle chaîne, ce qui permet d'améliorer les performances. Est-ce exact ?

1 votes

Oui, c'est exactement cela. Lors de l'affectation à m_name d'un const std::string& la mémoire interne est réutilisée tant que le paramètre m_name s'inscrit dans. Lors de l'attribution de mouvements m_name la mémoire doit être désallouée au préalable. Sinon, il est impossible de "voler" les ressources du côté droit de l'affectation.

0 votes

Quand devient-elle une référence pendante ? Je pense que la liste d'initialisation utilise la copie profonde.

1voto

LogicStuff Points 10924

Comment que vous transmettez n'est pas la seule variable ici, ce que que vous passez fait la grande différence entre les deux.

En C++, nous avons toutes sortes de catégories de valeurs et cet "idiome" existe pour les cas où l'on transmet un rvaleur (par exemple "Alex-string-literal-that-constructs-temporary-std::string" o std::move(nameString) ), ce qui se traduit par 0 exemplaires de std::string (le type n'a même pas besoin d'être copiable pour les arguments de type rvalue), et n'utilise que le type std::string du constructeur de mouvement.

Questions et réponses en rapport avec le sujet .

0voto

VTT Points 27056

L'approche "pass-by-value-and-move" présente plusieurs inconvénients par rapport à l'approche "pass-by-(rv)reference" :

  • il provoque l'apparition de 3 objets au lieu de 2 ;
  • Le passage d'un objet par sa valeur peut entraîner une surcharge de la pile, car même une classe de chaîne normale est généralement au moins 3 ou 4 fois plus grande qu'un pointeur ;
  • la construction des objets argumentaires se fera du côté de l'appelant, ce qui entraînera un gonflement du code ;

0 votes

Pourriez-vous préciser pourquoi 3 objets sont générés ? D'après ce que j'ai compris, je peux simplement passer "Peter" en tant que chaîne de caractères. Celle-ci serait créée, copiée puis déplacée, n'est-ce pas ? Et la pile ne serait-elle pas utilisée à un moment ou à un autre ? Pas au moment de l'appel au constructeur, mais dans la fonction m_name{name} où il est copié ?

0 votes

@Blackbot Je faisais référence à votre exemple std::string nameString("Alex"); Creature c(nameString); un objet est nameString Un autre est un argument de fonction, et le troisième est un champ de classe.

-1voto

Adam Getchell Points 355

Dans mon cas, le passage à la méthode pass by value et l'exécution d'un std:move ont provoqué une erreur de type "heap-use-after-free" dans Address Sanitizer.

https://travis-ci.org/github/acgetchell/CDT-plusplus/jobs/679520360#L3165

Je l'ai donc désactivé, ainsi que la suggestion de clang-tidy.

https://github.com/acgetchell/CDT-plusplus/compare/80c96789f0a2...0d78fd63b332

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X