Comme @ JDługosz le souligne dans les commentaires, Herb donne d'autres conseils dans une autre (plus tard?) Conférence, voir à peu près d'ici: https://youtu.be/xnqTKD8uD64?t=54m50s .
Son conseil se résume à n'utiliser que des paramètres de valeur pour une fonction f
qui prend ce qu'on appelle des arguments puits, en supposant que vous déplacerez la construction à partir de ces arguments puits.
Cette approche générale n'ajoute que la surcharge d'un constructeur de déplacement pour les arguments lvalue et rvalue par rapport à une implémentation optimale des f
arguments lvalue et rvalue respectivement. Pour voir pourquoi c'est le cas, supposons f
prend un paramètre de valeur, où T
est un type constructible de copie et de déplacement:
void f(T x) {
T y{std::move(x)};
}
L'appel f
avec un argument lvalue entraînera un constructeur de copie appelé pour construire x
et un constructeur de mouvement appelé pour construire y
. D'un autre côté, l'appel f
avec un argument rvalue entraînera l' appel d'un constructeur de mouvement pour construire x
et celui d'un autre constructeur de déplacement pour construire y
.
En général, l'implémentation optimale des f
arguments for lvalue est la suivante:
void f(const T& x) {
T y{x};
}
Dans ce cas, un seul constructeur de copie est appelé pour construire y
. L'implémentation optimale des f
arguments for rvalue est, encore une fois en général, la suivante:
void f(T&& x) {
T y{std::move(x)};
}
Dans ce cas, un seul constructeur de déplacement est appelé pour construire y
.
Un compromis raisonnable consiste donc à prendre un paramètre de valeur et à demander à un constructeur de déplacement supplémentaire d'appeler les arguments lvalue ou rvalue par rapport à l'implémentation optimale, ce qui est également le conseil donné dans le discours de Herb.
Comme @ JDługosz l'a souligné dans les commentaires, le passage par valeur n'a de sens que pour les fonctions qui construiront un objet à partir de l'argument puits. Lorsque vous avez une fonction f
qui copie son argument, l'approche passe-par-valeur aura plus de surcharge qu'une approche générale passe-par-const-référence. L'approche passe-par-valeur pour une fonction f
qui conserve une copie de son paramètre aura la forme:
void f(T x) {
T y{...};
...
y = std::move(x);
}
Dans ce cas, il existe une construction de copie et une affectation de déplacement pour un argument lvalue, ainsi qu'une construction de déplacement et une affectation de déplacement pour un argument rvalue. Le cas le plus optimal pour un argument lvalue est:
void f(const T& x) {
T y{...};
...
y = x;
}
Cela se résume à une affectation uniquement, ce qui est potentiellement beaucoup moins cher que le constructeur de copie plus l'affectation de déplacement requise pour l'approche de passage par valeur. La raison en est que l'affectation peut réutiliser la mémoire allouée existante dansy
, et donc empêcher les (dé) allocations, tandis que le constructeur de copie alloue généralement de la mémoire.
Pour un argument rvalue, l'implémentation la plus optimale pour f
celle qui conserve une copie a la forme:
void f(T&& x) {
T y{...};
...
y = std::move(x);
}
Donc, seulement une affectation de déplacement dans ce cas. Passer une valeur r à la version f
qui prend une référence const ne coûte qu'une affectation au lieu d'une affectation de déplacement. Donc, relativement parlant, la version def
prendre une référence const dans ce cas comme l'implémentation générale est préférable.
Donc, en général, pour la mise en œuvre la plus optimale, vous devrez surcharger ou faire une sorte de transfert parfait, comme indiqué dans l'exposé. L'inconvénient est une explosion combinatoire du nombre de surcharges requises, en fonction du nombre de paramètres pour le f
cas où vous opteriez pour une surcharge sur la catégorie de valeur de l'argument. Le transfert parfait a l'inconvénient de f
devenir une fonction de modèle, ce qui empêche de le rendre virtuel et entraîne un code beaucoup plus complexe si vous voulez le faire à 100% (voir le discours pour les détails sanglants).