Réponse courte d'abord : l'appel par const& coûtera toujours une copie. Selon les conditions l'appel par valeur pourrait ne coûter qu'un coup . Mais cela dépend (veuillez consulter les exemples de code ci-dessous pour le scénario auquel ce tableau fait référence) :
lvalue rvalue unused lvalue unused rvalue
------------------------------------------------------
const& copy copy - -
rvalue&& - move - -
value copy, move move copy -
T&& copy move - -
overload copy move - -
Donc, mon résumé exécutif serait que l'appel par valeur vaut la peine d'être considéré si
- Le déplacement est bon marché, car il peut y avoir un déplacement supplémentaire.
- le paramètre est utilisé sans condition. L'appel par valeur coûte également une copie si le paramètre n'est pas utilisé, par exemple à cause d'une clause if ou autre.
Appel par valeur
Considérons une fonction qui est utilisée pour copier son argument
class Dog {
public:
void name_it(const std::string& newName) { names.push_back(newName); }
private:
std::vector<std::string> names;
};
Dans le cas d'une lvalue passée à name_it
vous aurez aussi deux opérations de copie dans le cas d'une rvalue. C'est mauvais car la rvalue pourrait être déplacée.
Une solution possible serait d'écrire une surcharge pour rvalues :
class Dog {
public:
void name_it(const std::string& newName) { names.push_back(newName); }
void name_it(std::string&& newName) { names.push_back(std::move(newName)); }
private:
std::vector<std::string> names;
};
Cela résout le problème et tout va bien, malgré le fait que vous avez deux fonctions avec exactement le même code.
Une autre solution viable serait d'utiliser la redirection parfaite, mais cela présente aussi plusieurs inconvénients (par exemple, les fonctions de redirection parfaite sont assez gourmandes et rendent inutile une fonction const& surchargée existante, elles doivent généralement être dans un fichier d'en-tête, elles créent plusieurs fonctions dans le code objet et bien d'autres encore).
class Dog {
public:
template<typename T>
void name_it(T&& in_name) { names.push_back(std::forward<T>(in_name)); }
private:
std::vector<std::string> names;
};
Encore une autre solution serait d'utiliser appel par valeur :
class Dog {
public:
void name_it(std::string newName) { names.push_back(std::move(newName)); }
private:
std::vector<std::string> names;
};
L'important est que, comme vous l'avez mentionné, le std::move
. De cette façon, vous aurez une seule fonction pour les deux valeurs (rvalue et lvalue). Vous déplacerez les rvalues, mais accepterez un déplacement supplémentaire pour les lvalues, ce qui pourrait être correct. si le déménagement n'est pas cher et vous copiez ou déplacez le paramètre indépendamment des conditions.
En fin de compte, je pense vraiment que c'est une erreur de recommander une méthode plutôt qu'une autre. Cela dépend fortement.
#include <vector>
#include <iostream>
#include <utility>
using std::cout;
class foo{
public:
//constructor
foo() {}
foo(const foo&) { cout << "\tcopy\n" ; }
foo(foo&&) { cout << "\tmove\n" ; }
};
class VDog {
public:
VDog(foo name) : _name(std::move(name)) {}
private:
foo _name;
};
class RRDog {
public:
RRDog(foo&& name) : _name(std::move(name)) {}
private:
foo _name;
};
class CRDog {
public:
CRDog(const foo& name) : _name(name) {}
private:
foo _name;
};
class PFDog {
public:
template <typename T>
PFDog(T&& name) : _name(std::forward<T>(name)) {}
private:
foo _name;
};
//
volatile int s=0;
class Dog {
public:
void name_it_cr(const foo& in_name) { names.push_back(in_name); }
void name_it_rr(foo&& in_name) { names.push_back(std::move(in_name));}
void name_it_v(foo in_name) { names.push_back(std::move(in_name)); }
template<typename T>
void name_it_ur(T&& in_name) { names.push_back(std::forward<T>(in_name)); }
private:
std::vector<foo> names;
};
int main()
{
std::cout << "--- const& ---\n";
{
Dog a,b;
foo my_foo;
std::cout << "lvalue:";
a.name_it_cr(my_foo);
std::cout << "rvalue:";
b.name_it_cr(foo());
}
std::cout << "--- rvalue&& ---\n";
{
Dog a,b;
foo my_foo;
std::cout << "lvalue: -\n";
std::cout << "rvalue:";
a.name_it_rr(foo());
}
std::cout << "--- value ---\n";
{
Dog a,b;
foo my_foo;
std::cout << "lvalue:";
a.name_it_v(my_foo);
std::cout << "rvalue:";
b.name_it_v(foo());
}
std::cout << "--- T&&--\n";
{
Dog a,b;
foo my_foo;
std::cout << "lvalue:";
a.name_it_ur(my_foo);
std::cout << "rvalue:";
b.name_it_ur(foo());
}
return 0;
}
Salida:
--- const& ---
lvalue: copy
rvalue: copy
--- rvalue&& ---
lvalue: -
rvalue: move
--- value ---
lvalue: copy
move
rvalue: move
--- T&&--
lvalue: copy
rvalue: move
1 votes
Cela vous aide-t-il ?
1 votes
Considérez l'expression de l'argument rvalue par rapport à lvalue. Mais considérez également le passage d'un argument vers le bas d'une hiérarchie d'appel, ce qui est la situation habituelle.
0 votes
@Barry Gotcha. Il s'agit donc essentiellement d'une optimisation dans les cas où un temporaire est transmis et que l'on aurait pu simplement l'attraper à partir de celui-ci. Si je comprends bien, pour les valeurs l non temporaires, cela ne fera pas de différence.
0 votes
Je n'ai jamais vu ce premier cas. J'ai vu
Dog::Dog(std::string&& name) : _name(std::forward(name)) {}
qui utiliserait le constructeur move si possible.1 votes
@SilvioMayolo
name
n'est pas une référence de transfert dans votre exemple.2 votes
Lorsque vous passez par valeur, il est possible que vous puissiez vous contenter de deux mouvements et d'aucune copie. Avec la référence const il faut toujours au moins une copie.
0 votes
Il est intéressant de noter que cela déplace toutes les instructions de copie nécessaires de l'intérieur de la fonction vers tous les sites d'appel. Cela augmente la taille du binaire produit et peut potentiellement réduire les performances puisque le code répétitif occupe de l'espace dans les caches du CPU. Pour les petits programmes, ce n'est peut-être pas un gros problème, mais pour les bases de code de plusieurs millions de lignes, cela peut entraîner une augmentation de la taille du binaire et une baisse des performances qui peuvent être inacceptables.
0 votes
Vous négligez la caractéristique essentielle, à savoir que la fonction va modifier ou stocker le paramètre, indépendamment de l'argument.