En C++ traditionnel, le passage par valeur dans les fonctions et méthodes est lent pour les gros objets et est généralement mal vu. Au lieu de cela, les programmeurs C++ ont tendance à transmettre des références, ce qui est plus rapide, mais qui introduit toutes sortes de questions complexes sur la propriété et en particulier sur la gestion de la mémoire (dans le cas où l'objet est alloué par segment de mémoire)
Maintenant, en C++ 11, nous avons des références Rvalue et des constructeurs de déplacement, ce qui signifie qu'il est possible d'implémenter un grand objet (comme un std::vector
) ce n'est pas cher de passer par la valeur dans et hors d'une fonction.
Donc, cela signifie-t-il que la valeur par défaut devrait être de passer par valeur pour les instances de types tels que std::vector
et std::string
? Et pour les objets personnalisés? Quelle est la nouvelle meilleure pratique?
C'est un défaut raisonnable si vous devez faire une copie à l'intérieur du corps. C'est ce que Dave Abrahams préconise :
Directive: ne copiez pas vos arguments de fonction. Au lieu de cela, passez-les par valeur et laissez le compilateur faire la copie.
Dans le code, cela signifie ne pas faire cela:
void foo(T const& t)
{
auto copy = t;
// ...
}
mais faites ceci:
void foo(T t)
{
// ...
}
ce qui a l'avantage que l'appelant peut utiliser foo
comme ceci:
T lval;
foo(lval); // copy from lvalue
foo(T {}); // (potential) move from prvalue
foo(std::move(lval)); // (potential) move from xvalue
et seul un travail minimal est effectué. Vous auriez besoin de deux surcharges pour faire de même avec les références, void foo(T const&);
et void foo(T&&);
.
Dans cet esprit, j'ai maintenant écrit mes précieux constructeurs en tant que tels:
class T {
U u;
V v;
public:
T(U u, V v)
: u(std::move(u))
, v(std::move(v))
{}
};
Sinon, passer par référence à const
est toujours raisonnable.
Dans presque tous les cas, votre sémantique doit être soit:
bar(foo f); // want to obtain a copy of f
bar(const foo& f); // want to read f
bar(foo& f); // want to modify f
Toutes les autres signatures ne doivent être utilisées qu'avec parcimonie et avec une bonne justification. Le compilateur va maintenant à peu près toujours les résoudre de la manière la plus efficace. Vous pouvez simplement continuer à écrire votre code!
Passez les paramètres par valeur si à l'intérieur du corps de la fonction, vous avez besoin d'une copie de l'objet ou devez simplement déplacer l'objet. Passez par const&
si vous n'avez besoin que d'un accès non mutant à l'objet.
Exemple de copie d'objet:
void copy_antipattern(T const& t) { // (Don't do this.)
auto copy = t;
t.some_mutating_function();
}
void copy_pattern(T t) { // (Do this instead.)
t.some_mutating_function();
}
Exemple de déplacement d'objet:
std::vector<T> v;
void move_antipattern(T const& t) {
v.Push_back(t);
}
void move_pattern(T t) {
v.Push_back(std::move(t));
}
Exemple d'accès non mutant:
void read_pattern(T const& t) {
t.some_const_function();
}
Pour la justification, voir ces articles de blog par Dave Abrahams et Xiang Fan .
La signature d'une fonction doit refléter son utilisation prévue. La lisibilité est importante, également pour l'optimiseur.
C'est la meilleure condition préalable pour qu'un optimiseur crée le code le plus rapidement - en théorie du moins et sinon en réalité puis dans quelques années.
Les considérations de performances sont très souvent surestimées dans le contexte du passage de paramètres. Une transmission parfaite en est un exemple. Des fonctions comme emplace_back
sont pour la plupart très courts et alignés de toute façon.