Je veux réduire la copie au minimum, en particulier pour les types / classes coûteux. Je pourrais donc définir des surcharges comme ceci:
void SetParams(std::map<int, std::string> params) { mParams = std::move(params); } void SetBoardInfo(DataInfoStruct board_info) { mBoardInfo = std::move(board_info); }
dans une classe qui a une variable membre std :: map appelée mParams et un membre de structure DataInfoStruct appelé mBoardInfo.
Depuis DataInfoStruct n'a aucun constructeur ou destructeur défini par l'utilisateur. Je suppose qu'il a un constructeur de déplacement implicite.
Ensuite, je peux invoquer l'une des surcharges avec une lvalue ou une rvalue. Cependant, je pourrais avoir juste une version de chaque fonction comme ceci:
void SetParams(const std::map<int, std::string> ¶ms) { mParams = parameters; } void SetParams(std::map<int, std::string> &¶ms) { mParams = std::move(params); } void SetBoardInfo(const DataInfoStruct &board_info) { mBoardInfo = board_info; } void SetBoardInfo(DataInfoStruct &&board_info) { mBoardInfo = std::move(board_info); }
Dans un second temps, je créerai toujours une copie parce que je passe par valeur, mais ensuite je bouge le paramètre dans la variable membre. Si je passe une rvalue, je ne copie pas du tout, mais pour une lvalue, il y aura toujours une copie - ce que je ferai de toute façon si j'ai utilisé la première méthode.
Laquelle des deux méthodes est recommandé ? Les références rvalue sont-elles mieux utilisées uniquement pour les constructeurs et les opérateurs d'affectation?
4 Réponses :
La recommandation générale est la première manière. La seconde manière oblige à faire une allocation, alors que ce n'est pas toujours nécessaire. Pour un exemple:
value allocate: 5 allocate: 5 allocate: 3 allocate: 3 allocate: 3 reference allocate: 5
Ceci produit la sortie:
#include <vector> #include <iostream> bool my_log = false; struct my_alloc : std::allocator<int> { int* allocate(std::size_t n) { if (my_log) std::cout << "allocate: " << n << '\n'; return std::allocator<int>::allocate(n); } template<typename U> struct rebind { using other = my_alloc; }; }; using test_vec = std::vector<int, my_alloc>; struct foo { test_vec v_; void set_value(test_vec v) { v_ = v; } void set_ref(const test_vec& v) { v_ = v; } void set_ref(test_vec&& v) { v_ = v; } }; int main() { test_vec long_vec = { 0,1,2,3,4 }; test_vec short_vec = { 0,1,2 }; foo f; foo g; my_log = true; std::cout << "value\n"; f.set_value(long_vec); f.set_value(short_vec); f.set_value(short_vec); f.set_value(short_vec); std::cout << "reference\n"; g.set_ref(long_vec); g.set_ref(short_vec); g.set_ref(short_vec); g.set_ref(short_vec); }
Comme vous pouvez le voir, dans certaines circonstances, passer par valeur nécessite un supplément allocation à chaque fois, ce qui peut être évité si vous passez simplement par référence.
En bref, vous devez écrire une fonction set (const X &)
et une set (X &&) version à chaque fois. Il n'y a aucun moyen de le raccourcir.
Vous n'avez pas besoin d'une allocation si votre membre actuel dispose de suffisamment de stockage. C'est pourquoi la deuxième version est mauvaise - même si votre objet actuel a une capacité suffisante, vous forcez quand même un nouvel objet à être alloué.
Je suis d'accord sur cela, mais si c'est ce que vous voulez dire, vous devriez mal modifier cette réponse car elle est clairement incomplète et personne ne va le déduire en la lisant.
C'est une réponse intéressante. Cependant, dans votre exemple, vous n'appelez pas set_ref (test_vec && v) parce que vous n'utilisez pas std :: move ou que vous n'utilisez pas une valeur / r temporaire.
@jignatius Le but ici n'est pas de montrer l'intérêt de set_ref (test_vec)
par rapport à set_ref (test_vec &&)
, mais plutôt de montrer que si vous utilisez set_ref (test_vec) )
+ move
, vous devez toujours construire un nouvel objet, puis vous en éloigner, ce qui inclut toujours une allocation pour un std :: vector
(construction ), alors qu'avec set_ref (test_vec const &)
, vous n'affectez que, et l'allocation n'est pas requise si le vecteur que vous affectez est suffisamment grand.
Dans un second temps, je créerai toujours une copie car je passe par valeur
Non. L'argument peut être move construit à partir d'une xvalue ou initialisé directement à partir d'une prvalue (ce dernier uniquement depuis C ++ 17; avant cela, le déplacement peut être élidé comme une optimisation). Donc, même en créant un objet distinct (en ce qui concerne la machine abstraite), il n'y a pas nécessairement de copie.
Dans le cas d'une lvalue, l'argument sera copié; tout comme il sera copié dans l'option 1, auquel cas il appellera la surcharge de référence lvalue.
Laquelle des deux méthodes est recommandée?
Je recommande l'option 2 (passer par valeur) par défaut. Cela réduit de moitié la quantité de code standard, et la différence de performances entre une copie unique + un mouvement unique et une copie unique est généralement insignifiante.
Une exception à cela concerne les objets qui sont lents à se déplacer, comme un std :: array
avec de nombreux éléments. Je recommande l'option 1 (surcharges de référence) si vous avez mesuré un goulot d'étranglement causé par l'option 2 et constaté que 1 est plus rapide.
C'est ce que j'ai dit. La deuxième méthode ne copie que si je passe par référence à lvalue.
@jignatius "La deuxième méthode ne copie que si je passe par référence à lvalue." . Vous ne passez pas du tout par référence dans la deuxième méthode. Vous passez par valeur.
Accepter. Je passe par valeur (création d'une copie) en utilisant une lvalue.
@jignatius Dans la partie que j'ai citée, vous avez déclaré qu'une copie sera toujours créée. Ceci est une erreur. Les affirmations qui suivent sont correctes.
Pour une rvalue, aucune copie n'est effectuée.
@jignatius En effet. Cela contredit ce que vous dites dans la partie citée de la question.
D'accord. Pardon. Ma terminologie était un peu fausse. Je voulais vraiment dire passer par valeur, mais utiliser une lvalue - ce qui crée une copie.
La première version est généralement légèrement plus rapide mais elle introduit la duplication de code. La deuxième méthode est recommandée pour les objets peu coûteux à déplacer. Étant donné que la plupart des objets sont peu coûteux à déplacer en C ++, la deuxième méthode convient généralement. Mais, il y a une autre alternative:
template<typename T> void SetParams(T&& params) { mParams = std::forward<T>(params); } template<typename T> void SetBoardInfo(T&& board_info) { mBoardInfo = std::forward<T>(board_info); }
Merci à références universelles , c'est le meilleur des deux mondes s'il est acceptable d'avoir des setters modélisés.
exactement ce que j'avais en tête
Le seul inconvénient est que, comme il est maintenant basé sur un modèle, vous pouvez passer des trucs amusants avec. Si vous voulez également arrêter cela, peut-être enable_if requis pour une vérification de type plus rigide
Je ne veux pas utiliser de modèles ici car les paramètres ne peuvent être rien.
@jignatius, il est possible d'utiliser enable_if
pour faire votre propre vérification de type.
@jignatius consultez is_convertible et enable_if
Vous avez bien résumé les différentes approches. Merci.
Je veux réduire la copie au minimum, en particulier pour les types / classes coûteux.
Ne le faites pas. À moins que ces fonctions ne soient appelées des millions ou des milliards de fois, ou que votre carte de paramètres contienne des millions d'éléments, il est peu probable que cela prenne beaucoup de temps. À première vue, vous optimisez prématurément .
Si l'un de ces deux cas tient - ce dont je doute fort - vous perdez beaucoup plus de temps, et peut-être space, en utilisant un std :: map
- car il est extrêmement inefficace . Et une carte de chaînes allouées individuellement par tas, rien de moins!
Alors, allez-y simplement avec:
void SetParams(std::map<int, std::string> params) : mParams(parameters) { }
et laissez le compilateur s'en occuper .
PS - Vous devez toujours mesurer avant de vous lancer dans un effort d'optimisation, et en particulier avant de préférer les avantages supposés en termes de performances à la simplicité, à la lisibilité, DRY (ne vous répétez pas) et ainsi de suite.
lié / peut-être dupe: stackoverflow.com/questions / 57923963 /…
Vous pourriez écrire un livre à ce sujet ... Voici le point de départ d'une conférence Herb Sutter: youtu.be/xnqTKD8uD64 ? t = 3849 Je pense que c'est trop large pour SO.
@NathanOliver La question liée semble être uniquement liée au constructeur, pour lequel vous avez probablement des recommandations différentes, par exemple la méthode des setters (voir l'exposé de Herb dans mon commentaire précédent).