web-dev-qa-db-fra.com

Pour prendre en charge la sémantique de déplacement, les paramètres de fonction doivent-ils être pris par unique_ptr, par valeur ou par rvalue?

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?

17
MaxHeap

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
30
Steve Lorimer

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

14
Justin

Cela dépend de vos habitudes d'utilisation:

Option 1

Avantages: 

  • La responsabilité est explicitement exprimée et passée de l'appelant à l'appelé

Les inconvénients:

  • À moins que le vecteur ait déjà été encapsulé avec un unique_ptr, cela n’améliorera pas la lisibilité.
  • Les pointeurs intelligents gèrent en général les objets alloués dynamiquement. Ainsi, votre 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:

  • Lorsque l'appelant n'utilise pas 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:

  • Il résout tous les scénarios problématiques décrits pour les options 2 et 3 tout en bénéficiant également de leurs avantages.
  • Appelant a décidé de garder une copie pour lui-même ou non
  • Peut être optimisé pour n'importe quel scénario

Les inconvénients:

  • Si la méthode accepte de nombreux paramètres à la fois en tant que références const et/ou références rvalue, le nombre de prototypes augmente de manière exponentielle

Résumé:

Tant que vous n'avez pas de tels prototypes, c'est une excellente option.

6
Daniel Trugman

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));.

0
Andre Kostur