web-dev-qa-db-fra.com

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

J'essaie de comprendre la sémantique des mouvements, les références rvalue, std::move, Etc. J'ai essayé de comprendre, en cherchant à travers diverses questions sur ce site, pourquoi passer une fonction const std::string &name + _name(name) est moins recommandé qu'une 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)) {}

L'alternative (et à l'ancienne) est de la passer par référence et de la 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 déplace les deux, et la deuxième méthode 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?

31
John Bonata

Lorsque consommant données, vous aurez besoin d'un objet que vous pouvez consommer. Lorsque vous obtenez un std::string const& vous aurez devez copier l'objet indépendamment de la nécessité ou non de l'argument.

Lorsque l'objet est transmis par valeur, l'objet sera copié s'il doit être copié, c'est-à-dire lorsque l'objet transmis n'est pas temporaire. Cependant, s'il se trouve qu'il s'agit d'un objet temporaire, l'objet peut être construit sur place, c'est-à-dire que toutes les copies peuvent avoir été élidées et vous ne payez que pour une construction de déménagement. Autrement dit, il est possible qu'aucune copie ne se produise réellement.

30
Dietmar Kühl

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, cela nécessite exactement une copie, pour initialiser _name de name. Le déplacement n'est pas une option car name est const.

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

    Cela 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 valeur l, cela copiera pour passer l'argument, puis un mouvement pour remplir le membre de données. Lorsqu'il est appelé avec une valeur r, cela se déplacera pour passer l'argument, puis se déplacera pour remplir le membre de données. Dans le cas de la valeur r, le déplacement pour passer l'argument peut être élidé. Ainsi, l'appel avec une valeur l entraîne une copie et un déplacement et l'appel avec une valeur r entraîne un ou deux déplacements.

La solution optimale consiste à définir à la fois (1) et (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 à gérer 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 chaîne. Ensuite, nous tombons dans les cas rvalue. Dans ce cas, en utilisant un string_view fournit encore une autre option:

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

    Lorsqu'il est appelé avec une chaîne lvalue ou rvalue, cela se traduit par une copie. Lorsqu'il est appelé avec un const char*, un calcul de longueur a lieu et une copie.

26
Jeff Garrett

Réponse courte en premier: l'appel par const & coûtera toujours une copie. Selon les conditions l'appel par valeur peut 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 se réfère):

            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é serait que l'appel par valeur mérite d'être pris en compte si

  • le mouvement est bon marché, car il pourrait y avoir un mouvement 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 ex. en raison d'une clause if ou sth.

Appel par valeur

Considérons une fonction 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;
};

En cas de valeur l passée à name_it vous aurez également deux opérations de copie en cas de valeur r. C'est mal parce que la valeur pourrait me déplacer.

ne 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 ayez deux codes deux fonctions avec exactement le même code.

ne autre solution viable consisterait à utiliser un transfert parfait, mais cela présente également plusieurs inconvénients, (par exemple, les fonctions de transfert parfaites sont assez gourmandes et rendent inutile une fonction et une constante surchargées existantes, généralement elles devront être dans un en-tête fichier, ils créent plusieurs fonctions dans le code objet et quelques autres.)

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é std::move. De cette façon, vous aurez une fonction pour rvalue et lvalue. Vous déplacerez rvalues ​​mais accepterez un déplacement supplémentaire pour lvalues, ce qui pourrait être correct si le déplacement est bon marché et vous copiez ou déplacez le paramètre indépendamment des conditions.

Donc, à la fin, je pense vraiment qu'il est tout à fait erroné de recommander une solution par rapport aux autres. 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;
}

Production:

--- const& ---
lvalue: copy
rvalue: copy
--- rvalue&& ---
lvalue: -
rvalue: move
--- value ---
lvalue: copy
    move
rvalue: move
--- T&&--
lvalue: copy
rvalue: move
5
DrSvanHay

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.
0
warchantua

En dehors des raisons de performances, lorsqu'une copie lève une exception sur un constructeur par valeur, elle est lancée en premier sur l'appelant et non dans le constructeur lui-même. Cela permet de coder plus facilement les 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;
}

Sortira

Exception outside B2
Exception in C initializer
Exception outside C
0
Beached