Une de mes fonctions prend un vecteur en tant que paramètre et le stocke en tant que variable membre. J'utilise const référence à un vecteur comme décrit ci-dessous.
class Test {
public:
void someFunction(const std::vector<string>& items) {
m_items = items;
}
private:
std::vector<string> m_items;
};
Cependant, parfois items
contient un grand nombre de chaînes, j'aimerais donc ajouter une fonction (ou la remplacer par une nouvelle) prenant en charge la sémantique de déplacement.
Je pense à plusieurs approches, mais je ne sais pas laquelle choisir.
1) unique_ptr
void someFunction(std::unique_ptr<std::vector<string>> items) {
// Also, make `m_itmes` std::unique_ptr<std::vector<string>>
m_items = std::move(items);
}
2) passer par valeur et déplacer
void someFunction(std::vector<string> items) {
m_items = std::move(items);
}
3) rvalue
void someFunction(std::vector<string>&& items) {
m_items = std::move(items);
}
Quelle approche devrais-je éviter et pourquoi?
Sauf si vous avez une raison pour que le vecteur vive sur le tas, je vous déconseille d'utiliser unique_ptr
De toute façon, la mémoire interne du vecteur réside sur le tas, vous aurez donc besoin de 2 degrés d'indirection si vous utilisez unique_ptr
, un pour déréférencer le pointeur sur le vecteur et de nouveau pour déréférencer le tampon de stockage interne.
En tant que tel, je conseillerais d’utiliser soit 2, soit 3.
Si vous choisissez l'option 3 (nécessitant une référence à une valeur), vous imposez aux utilisateurs de votre classe l'obligation de transmettre une valeur (directement à partir d'une valeur temporaire ou à partir d'une valeur) lors de l'appel de someFunction
.
L'obligation de quitter une valeur est onéreuse.
Si vos utilisateurs veulent conserver une copie du vecteur, ils doivent sauter dans des cerceaux pour le faire.
std::vector<string> items = { "1", "2", "3" };
Test t;
std::vector<string> copy = items; // have to copy first
t.someFunction(std::move(items));
Toutefois, si vous optez pour l'option 2, l'utilisateur peut décider s'il souhaite conserver une copie ou non - le choix lui appartient.
Conservez une copie:
std::vector<string> items = { "1", "2", "3" };
Test t;
t.someFunction(items); // pass items directly - we keep a copy
Ne conservez pas de copie:
std::vector<string> items = { "1", "2", "3" };
Test t;
t.someFunction(std::move(items)); // move items - we don't keep a copy
En apparence, l'option 2 semble être une bonne idée car elle gère les valeurs lvalues et rvalues dans une seule fonction. Cependant, comme Herb Sutter le note dans son discours sur la CppCon 2014 Retour aux principes fondamentaux! Le style moderne C++} [ , il s’agit d’une pessimisation du cas courant de valeurs.
Si m_items
était "plus grand" que items
, votre code d'origine n'allouera pas de mémoire pour le vecteur:
// Original code:
void someFunction(const std::vector<string>& items) {
// If m_items.capacity() >= items.capacity(),
// there is no allocation.
// Copying the strings may still require
// allocations
m_items = items;
}
L'opérateur d'attribution de copie sur std::vector
est suffisamment intelligent pour réutiliser l'allocation existante. Par contre, prendre le paramètre par valeur devra toujours faire une autre allocation:
// Option 2:
// When passing in an lvalue, we always need to allocate memory and copy over
void someFunction(std::vector<string> items) {
m_items = std::move(items);
}
Pour le dire simplement: la construction de la copie et son affectation n’ont pas nécessairement le même coût. Il n’est pas improbable que l’affectation de copie soit plus efficace que la construction de copie - elle est plus efficace pour std::vector
et std::string
†.
La solution la plus simple, comme le note Herb, consiste à ajouter une surcharge rvalue (essentiellement votre option 3):
// You can add `noexcept` here because there will be no allocation‡
void someFunction(std::vector<string>&& items) noexcept {
m_items = std::move(items);
}
Notez que l'optimisation de l'attribution de copie ne fonctionne que lorsque m_items
existe déjà. Il est donc parfaitement correct de définir des paramètres sur constructeurs par valeur. L'attribution doit être effectuée dans les deux sens.
TL; DR: Choisissez to add option 3. Autrement dit, ayez une surcharge pour les lvalues et une pour les rvalues. L'option 2 force la copie construction au lieu de la copie affectation, ce qui peut coûter plus cher (et concerne std::string
et std::vector
).
† Si vous voulez voir des points de repère indiquant que l'option 2 peut être une pessimisation, à ce stade de la discussion , Herb affiche certains points de repère.
‡ Nous n’aurions pas dû indiquer noexcept
si l’opérateur d’affectation des déplacements de std::vector
n’était pas noexcept
. Consultez la documentation si vous utilisez un allocateur personnalisé.
En règle générale, sachez que des fonctions similaires ne doivent être marquées noexcept
que si le type de déplacement est assigné à noexcept
Cela dépend de vos habitudes d'utilisation:
Option 1
Avantages:
Les inconvénients:
unique_ptr
, cela n’améliorera pas la lisibilité.vector
doit en devenir un. Puisque les conteneurs de bibliothèque standard sont des objets gérés qui utilisent des allocations internes pour le stockage de leurs valeurs, cela signifie qu'il y aura deux allocations dynamiques pour chaque vecteur. Un pour le bloc de gestion de l'unique objet ptr + l'objet vector
lui-même et un autre pour les éléments stockés.Résumé:
Si vous gérez systématiquement ce vecteur à l'aide d'un unique_ptr
, continuez à l'utiliser, sinon ne le faites pas.
Option 2
Avantages:
Cette option est très flexible car elle permet à l’appelant de décider s’il souhaite ou non conserver une copie:
std::vector<std::string> vec { ... };
Test t;
t.someFunction(vec); // vec stays a valid copy
t.someFunction(std::move(vec)); // vec is moved
Lorsque l'appelant utilise std::move()
, l'objet n'est déplacé que deux fois (aucune copie), ce qui est efficace.
Les inconvénients:
std::move()
, un constructeur de copie est toujours appelé pour créer l'objet temporaire. Si nous utilisions void someFunction(const std::vector<std::string> & items)
et que notre m_items
était déjà suffisamment grand (en termes de capacité) pour accueillir items
, l'affectation m_items = items
n'aurait été qu'une opération de copie, sans l'allocation supplémentaire.Résumé:
Si vous savez à l'avance que cet objet va être re -set plusieurs fois pendant l'exécution, et que l'appelant n'utilise pas toujours std::move()
, je l'aurais évité. Sinon, il s'agit d'une excellente option, car elle est très flexible, permettant à la fois une convivialité et des performances supérieures à la demande malgré le scénario problématique.
Option 3
Les inconvénients:
Cette option oblige l'appelant à abandonner sa copie. Donc, s'il veut garder une copie pour lui, il doit écrire du code supplémentaire:
std::vector<std::string> vec { ... };
Test t;
t.someFunction(std::vector<std::string>{vec});
Résumé:
C'est moins flexible que l'option n ° 2 et je dirais donc inférieure dans la plupart des scénarios.
Option 4
Compte tenu des inconvénients des options 2 et 3, je suggérerais une option supplémentaire:
void someFunction(const std::vector<int>& items) {
m_items = items;
}
// AND
void someFunction(std::vector<int>&& items) {
m_items = std::move(items);
}
Avantages:
Les inconvénients:
Résumé:
Tant que vous n'avez pas de tels prototypes, c'est une excellente option.
Le conseil actuel à ce sujet est de prendre le vecteur par valeur et de le déplacer dans la variable membre:
void fn(std::vector<std::string> val)
{
m_val = std::move(val);
}
Et je viens de vérifier, std::vector
fournit un opérateur d'affectation de déménagement. Si l'appelant ne souhaite pas conserver une copie, il peut la transférer dans la fonction du site de l'appel: fn(std::move(vec));
.