J'apprends le C ++ en ce moment et j'essaie d'éviter de prendre de mauvaises habitudes. D'après ce que je comprends, clang-tidy contient de nombreuses «meilleures pratiques» et j'essaie de m'y tenir du mieux possible (même si je ne comprends pas nécessairement pourquoi elles sont encore considérées comme bonnes), mais je ne suis pas sûr si je 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}
{
}
};
Cela conduit à une suggestion de clang-tidy que je devrais passer par valeur au lieu de référence et d'utilisation std::move
. Si je le fais, je reçois la suggestion de faire name
une référence (pour assurer qu'il ne soit pas copié à chaque fois) et l'avertissement qui std::move
n'aura aucun effet parce name
est un const
donc je dois l' enlever.
La seule façon de ne pas recevoir d'avertissement est de supprimer const
complètement:
Creature(std::string name)
: m_name{std::move(name)}
{
}
Ce qui semble logique, car le seul avantage const
était d'éviter de jouer avec la chaîne d'origine (ce qui n'arrive pas parce que j'ai passé par valeur). Mais j'ai lu sur CPlusPlus.com :
Notez cependant 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é. Ce qui signifie qu'après une telle opération, la valeur de l'objet déplacé ne doit être détruite que ou affectée d'une nouvelle valeur; y accéder donne sinon une valeur non spécifiée.
Imaginez maintenant ce code:
std::string nameString("Alex");
Creature c(nameString);
Parce qu'il nameString
est passé par valeur, std::move
ne sera invalidé name
qu'à l'intérieur du constructeur et ne touchera pas la chaîne d'origine. Mais quels en sont les avantages? Il semble que le contenu ne soit copié qu'une seule fois de toute façon - si je passe par référence lorsque j'appelle m_name{name}
, si je passe par valeur lorsque je le passe (et ensuite il est déplacé). Je comprends que c'est mieux que de passer par valeur et de ne pas utiliser std::move
(car il est copié deux fois).
Donc deux questions:
- Ai-je bien compris ce qui se passe ici?
- Y a-t-il un avantage à utiliser
std::move
le dépassement par référence et simplement appelerm_name{name}
?
Creature c("John");
fait une copie supplémentairestd::string_view
et l'authentification unique.clang-tidy
un excellent moyen de devenir obsédé par les microoptimisations inutiles au détriment de la lisibilité. La question à se poser ici, avant toute autre chose, est de savoir combien de fois appelons-nous réellement leCreature
constructeur.Réponses:
Oui.
Une signature de fonction facile à saisir sans aucune surcharge supplémentaire. La signature révèle immédiatement que l'argument sera copié - cela évite aux appelants de se demander si une
const std::string&
référence pourrait être stockée en tant que membre de données, devenant éventuellement une référence pendante plus tard. Et il n'est pas nécessaire de surcharger les argumentsstd::string&& name
et lesconst std::string&
arguments pour éviter des copies inutiles lorsque des rvalues sont passées à la fonction. Passer une lvalueà la fonction qui prend son argument par valeur provoque une construction de copie et de déplacement. Passer une rvalue à la même fonction
provoque deux constructions de déplacement. En revanche, lorsque le paramètre de fonction est
const std::string&
, il y aura toujours une copie, même lors du passage d'un argument rvalue. C'est clairement un avantage tant que le type d'argument est bon marché à déplacer-construire (c'est le cas pourstd::string
).Mais il y a un inconvénient à considérer: le raisonnement ne fonctionne pas pour les fonctions qui affectent l'argument de la fonction à une autre variable (au lieu de l'initialiser):
provoquera une désallocation de la ressource à laquelle se
m_name
réfère avant sa réaffectation. Je recommande de lire l'article 41 dans Effective Modern C ++ et aussi cette question .la source
move
, l'espace est désalloué. Si je ne l'utilise pasmove
, il n'est désalloué que si l'espace alloué est trop petit pour contenir la nouvelle chaîne, ce qui améliore les performances. Est-ce exact?m_name
partir d'unconst std::string&
paramètre, la mémoire interne est réutilisée tant qu'ellem_name
tient. Lors de l'assignation de déplacementm_name
, la mémoire doit être désallouée au préalable. Sinon, il était impossible de «voler» les ressources du côté droit de la mission.Une lvalue passée se lie à
name
, puis est copiée dansm_name
.Une rvalue passée se lie à
name
, puis est copiée dansm_name
.Une lvalue passée est copiée dans
name
, puis déplacée dansm_name
.Une rvalue passée est déplacée dans
name
, puis est déplacée dansm_name
.Une lvalue passée se lie à
name
, puis est copiée dansm_name
.Une rvalue passée se lie à
rname
, puis est déplacée dansm_name
.Comme 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 optimal 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 un transfert parfait :
Vous pouvez éventuellement vouloir contraindre
T
afin de restreindre le domaine des types avec lesquels ce constructeur peut être instancié (comme indiqué ci-dessus). C ++ 20 vise à simplifier cela avec Concepts .En C ++ 17, les valeurs prvalues sont affectées par l' élision de copie garantie , qui - le cas échéant - réduira le nombre de copies / déplacements lors du passage d'arguments aux fonctions.
la source
Creature(const std::string &name) : m_name{std::move(name)} { }
dans le (2) ?Comment vous passez n'est pas la seule variable ici, ce que vous passez fait la grande différence entre les deux.
En C ++, nous avons toutes sortes de catégories de valeur et ce « langage » existe pour les cas où l' on passe dans un rvalue ( par exemple
"Alex-string-literal-that-constructs-temporary-std::string"
oustd::move(nameString)
), dont les résultats en 0 exemplaires destd::string
cours (le genre ne dispose même pas d'être constructible copie pour les arguments rvalue), et utilise uniquementstd::string
le constructeur de déplacement de.Questions et réponses quelque peu liées .
la source
Il y a plusieurs inconvénients de l'approche pass-by-value-and-move par rapport à la référence pass-by-(RV):
la source
m_name{name}
partie où il est copié?std::string nameString("Alex"); Creature c(nameString);
un objet estnameString
, un autre est un argument de fonction et le troisième est un champ de classe.Dans mon cas, le passage au passage par valeur, puis l'exécution d'un std: move ont provoqué une erreur d'utilisation du tas après la libération dans Address Sanitizer.
https://travis-ci.org/github/acgetchell/CDT-plusplus/jobs/679520360#L3165
Donc, je l'ai désactivé, ainsi que la suggestion en clang-tidy.
https://github.com/acgetchell/CDT-plusplus/compare/80c96789f0a2...0d78fd63b332
la source