36 votes

Pourquoi le passage par valeur (si une copie est nécessaire) est-il recommandé en C++11 si une référence constante ne coûte qu'une seule copie également ?

J'essaie de comprendre la sémantique des déplacements, les références rvalue, std::move etc. J'ai essayé de comprendre, en cherchant parmi les diverses questions posées sur ce site, pourquoi le fait de passer une const std::string &name + _name(name) est moins recommandé qu'un std::string name + _name(std::move(name)) si une copie est nécessaire.

Si je comprends bien, ce qui suit nécessite une seule copie (via le constructeur) plus un déplacement (du temporaire vers le membre) :

Dog::Dog(std::string name) : _name(std::move(name)) {}

La méthode alternative (et ancienne) consiste à le passer par référence et à le copier (de la référence au membre) :

Dog::Dog(const std::string &name) : _name(name) {}

Si la première méthode nécessite une copie et un déplacement, alors que la seconde ne nécessite qu'une seule copie, comment la première méthode peut-elle être préférée et, dans certains cas, plus rapide ?

1 votes

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.

33voto

Dietmar Kühl Points 70604

Lorsque en consommant vous aurez besoin d'un objet que vous pourrez consommer. Lorsque vous obtenez un std::string const& usted se doivent copier l'objet indépendamment du fait que l'argument sera nécessaire ou non.

Lorsque l'objet est passé par valeur, l'objet sera copié s'il doit être copié, c'est-à-dire lorsque l'objet passé n'est pas un temporaire. Cependant, s'il s'agit d'un temporaire, l'objet peut être construit sur place, c'est-à-dire que toute copie peut avoir été élidée et que vous payez juste pour une construction de déplacement. C'est-à-dire qu'il y a une chance qu'aucune copie ne se produise réellement.

8 votes

Il y a un troisième cas ; si l'objet passé est une référence rvalue (c'est-à-dire, la valeur de retour de std move). Alors by-value fait 2 déplacements et 0 copie, alors que const ref fait 1 copie.

0 votes

En ce qui concerne votre première phrase, je suis confus : Pourquoi devrez-vous copier l'objet dans tous les cas ? L'intérêt de passer une référence est justement de ne pas avoir à copier l'objet auquel on se réfère.

0 votes

@Alex : notez la condition : "Quand en consommant ...", c'est-à-dire lorsque vous utilisez l'objet pour en initialiser un autre. Dans ce cas, vous avez besoin d'une copie mais vous savez [normalement] à l'avance que cela va se produire. Ne rien faire (en raison de l'élision de la copie) ou créer une copie qui peut ensuite être déplacée (lorsque l'élision de la copie ne peut pas être utilisée) est l'approche la plus efficace lorsque la copie est de toute façon nécessaire.

30voto

Jeff Garrett Points 1115

Pensez à appeler les différentes options avec une lvalue et une rvalue :

  1. Dog::Dog(const std::string &name) : _name(name) {}

    Qu'il soit appelé avec une lvalue ou une rvalue, il nécessite exactement une copie, pour initialiser _name de name . Le déménagement n'est pas une option car name es const .

  2. Dog::Dog(std::string &&name) : _name(std::move(name)) {}

    Ceci ne peut être appelé qu'avec une valeur r, et il se déplacera.

  3. Dog::Dog(std::string name) : _name(std::move(name)) {}

    Lorsqu'il est appelé avec une lvalue, il copiera pour passer l'argument et ensuite un mouvement pour remplir le membre de données. Lorsqu'elle est appelée avec une rvalue, elle se déplace pour passer l'argument, puis se déplace pour remplir le membre de données. Dans le cas de la rvalue, le déplacement pour passer l'argument peut être élidé. Ainsi, l'appel de cette fonction avec une lvalue entraîne une copie et un déplacement, et l'appel de cette fonction avec une rvalue entraîne un à deux déplacements.

La solution optimale consiste à définir les deux (1) y (2) . Solution (3) peut avoir un mouvement supplémentaire par rapport à l'optimum. Mais l'écriture d'une fonction est plus courte et plus facile à maintenir que l'écriture de deux fonctions pratiquement identiques, et les déplacements sont supposés être bon marché.

Lors d'un appel avec une valeur implicitement convertible en chaîne comme const char* la conversion implicite a lieu, ce qui implique un calcul de longueur et une copie des données de la chaîne. Nous tombons alors dans les cas de rvalue. Dans ce cas, l'utilisation d'un string_view offre une autre option :

  1. Dog::Dog(std::string_view name) : _name(name) {}

    Lorsqu'il est appelé avec une chaîne de caractères lvalue ou rvalue, il en résulte une copie. Lorsqu'il est appelé avec un const char* un calcul de longueur a lieu et une copie.

0 votes

+1 Vous pourriez faire remarquer que le raisonnement ne s'applique pas nécessairement aux fonctions setter ou aux opérateurs d'affectation (en fait, il s'applique surtout aux constructeurs). Voir stackoverflow.com/questions/18303287/

1 votes

Vous avez envisagé la possibilité que Dog est en cours de construction avec soit une valeur r std::string ou une lvalue std::string mais en plus, il est possible qu'il soit construit avec quelque chose d'implicitement convertible en std::string à savoir un const char* . (Les autres réponses aussi, mais la vôtre est la plus proche de l'exhaustivité).

0 votes

Pourquoi dans l'affaire (4) When called with a string lvalue or rvalue, this results in one copy. ? une copie se produit lorsque l'on lie le paramètre à name et il devrait y en avoir un autre qui se produit lors de l'initialisation. _name ? Merci

9voto

DrSvanHay Points 1085

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

0voto

Beached Points 559

En dehors des raisons de performance, lorsqu'une copie lève une exception sur un constructeur par valeur, elle est levée d'abord sur l'appelant et non dans le constructeur lui-même. Cela permet de coder plus facilement des constructeurs noexcept et de ne pas avoir à se soucier des fuites de ressources ou d'un bloc try/catch sur un constructeur.

struct A {
    std::string a;

    A( ) = default;
    ~A( ) = default;
    A( A && ) noexcept = default;
    A &operator=( A && ) noexcept = default;

    A( A const &other ) : a{other.a} {
        throw 1;
    }
    A &operator=( A const &rhs ) {
        if( this != &rhs ) {
            a = rhs.a;
            throw 1;
        }
        return *this;
    }
};

struct B {
    A a;

    B( A value ) try : a { std::move( value ) }
    { std::cout << "B constructor\n"; }
    catch( ... ) {
        std::cerr << "Exception in B initializer\n";
    }
};

struct C {
    A a;

    C( A const &value ) try : a { value }
    { std::cout << "C constructor\n"; }
    catch( ... ) {
        std::cerr << "Exception in C initializer\n";
    }
};

    int main( int, char ** ) {

    try {
        A a;
        B b{a};
    } catch(...) { std::cerr << "Exception outside B2\n"; }

    try {
        A a;
        C c{a};
    } catch(...) { std::cerr << "Exception outside C\n"; }

    return EXIT_SUCCESS;
}

La sortie sera

Exception outside B2
Exception in C initializer
Exception outside C

0voto

warchantua Points 78

J'ai fait une expérience :

#include <cstdio>
#include <utility>

struct Base {
  Base() { id++; }
  static int id;
};

int Base::id = 0;

struct Copyable : public Base {
  Copyable() = default;
  Copyable(const Copyable &c) { printf("Copyable [%d] is copied\n", id); }
};

struct Movable : public Base {
  Movable() = default;

  Movable(Movable &&m) { printf("Movable [%d] is moved\n", id); }
};

struct CopyableAndMovable : public Base {
  CopyableAndMovable() = default;

  CopyableAndMovable(const CopyableAndMovable &c) {
    printf("CopyableAndMovable [%d] is copied\n", id);
  }

  CopyableAndMovable(CopyableAndMovable &&m) {
    printf("CopyableAndMovable [%d] is moved\n", id);
  }
};

struct TEST1 {
  TEST1() = default;
  TEST1(Copyable c) : q(std::move(c)) {}
  TEST1(Movable c) : w(std::move(c)) {}
  TEST1(CopyableAndMovable c) : e(std::move(c)) {}

  Copyable q;
  Movable w;
  CopyableAndMovable e;
};

struct TEST2 {
  TEST2() = default;
  TEST2(Copyable const &c) : q(c) {}
  //  TEST2(Movable const &c) : w(c)) {}
  TEST2(CopyableAndMovable const &c) : e(std::move(c)) {}

  Copyable q;
  Movable w;
  CopyableAndMovable e;
};

int main() {
  Copyable c1;
  Movable c2;
  CopyableAndMovable c3;
  printf("1\n");
  TEST1 z(c1);
  printf("2\n");
  TEST1 x(std::move(c2));
  printf("3\n");
  TEST1 y(c3);

  printf("4\n");
  TEST2 a(c1);
  printf("5\n");
  TEST2 s(c3);

  printf("DONE\n");
  return 0;
}

Et voici le résultat :

1
Copyable [4] is copied
Copyable [5] is copied
2
Movable [8] is moved
Movable [10] is moved
3
CopyableAndMovable [12] is copied
CopyableAndMovable [15] is moved
4
Copyable [16] is copied
5
CopyableAndMovable [21] is copied
DONE

Conclusion :

template <typename T>
Dog::Dog(const T &name) : _name(name) {} 
// if T is only copyable, then it will be copied once
// if T is only movable, it results in compilation error (conclusion: define separate move constructor)
// if T is both copyable and movable, it results in one copy

template <typename T>
Dog::Dog(T name) : _name(std::move(name)) {}
// if T is only copyable, then it results in 2 copies
// if T is only movable, and you called Dog(std::move(name)), it results in 2 moves
// if T is both copyable and movable, it results in one copy, then one move.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X