145 votes

Le pass-by-value est-il un défaut raisonnable en C++11 ?

Dans le C++ traditionnel, le passage par valeur dans les fonctions et les méthodes est lent pour les gros objets, et est généralement mal vu. Au lieu de cela, les programmeurs C++ ont tendance à passer des références, ce qui est plus rapide, mais introduit toutes sortes de questions compliquées concernant la propriété et surtout la gestion de la mémoire (dans le cas où l'objet est alloué au tas).

Maintenant, en C++11, nous avons des références Rvalue et des constructeurs mobiles, ce qui signifie qu'il est possible d'implémenter un objet de grande taille (comme un std::vector ) qui est bon marché pour passer par valeur dans et hors d'une fonction.

Cela signifie-t-il que la valeur par défaut devrait être le passage par valeur pour les instances des types tels que std::vector et std::string ? Qu'en est-il des objets personnalisés ? Quelle est la nouvelle meilleure pratique ?

23 votes

pass by reference ... which introduces all sorts of complicated questions around ownership and especially around memory management (in the event that the object is heap-allocated) . Je ne comprends pas en quoi c'est compliqué ou problématique pour la propriété ? Peut-être ai-je manqué quelque chose ?

1 votes

@iammilind : Un exemple tiré de mon expérience personnelle. Un fil a un objet chaîne. Il est passé à une fonction qui génère un autre thread, mais sans que l'appelant le sache, la fonction a pris la chaîne de caractères en tant que const std::string& et non une copie. Le premier fil s'est ensuite éteint...

12 votes

@ZanLynx : Cela ressemble à une fonction qui n'a clairement jamais été conçue pour être appelée comme une fonction de fil.

142voto

Luc Danton Points 21421

C'est un défaut raisonnable si vous devez faire une copie à l'intérieur du corps. C'est ce que Dave Abrahams préconise :

Ligne directrice : Ne copiez pas les arguments de vos fonctions. Passez-les plutôt par valeur et laissez le compilateur se charger de la copie.

En code, cela signifie "ne faites pas ça" :

void foo(T const& t)
{
    auto copy = t;
    // ...
}

mais faites ça :

void foo(T t)
{
    // ...
}

qui a l'avantage de permettre à l'appelant d'utiliser foo comme ça :

T lval;
foo(lval); // copy from lvalue
foo(T {}); // (potential) move from prvalue
foo(std::move(lval)); // (potential) move from xvalue

et seul un travail minimal est effectué. Il faudrait deux surcharges pour faire la même chose avec les références, void foo(T const&); et void foo(T&&); .

Avec cela en tête, j'ai maintenant écrit mes constructeurs de valeur comme tel :

class T {
    U u;
    V v;
public:
    T(U u, V v)
        : u(std::move(u))
        , v(std::move(v))
    {}
};

Sinon, le passage par référence à const est encore raisonnable.

29 votes

+1, surtout pour la dernière partie :) Il ne faut pas oublier que les constructeurs de déplacement ne peuvent être invoqués que si l'objet à déplacer n'est pas censé rester inchangé par la suite : SomeProperty p; for (auto x: vec) { x.foo(p); } ne convient pas, par exemple. De plus, les constructeurs de mouvements ont un coût (plus l'objet est grand, plus le coût est élevé), tandis que les constructeurs d'objets sont des objets à part entière. const& sont essentiellement gratuits.

25 votes

@MatthieuM. Mais il est important de savoir ce que signifie réellement "plus l'objet est grand, plus le coût" du déménagement est élevé : "plus grand" signifie en fait "plus il a de variables membres". Par exemple, le déplacement d'un std::vector avec un million d'éléments les mêmes coûts comme le déplacement d'un tableau à cinq éléments puisque seul le pointeur vers le tableau sur le tas est déplacé, et non tous les objets du vecteur. Ce n'est donc pas un gros problème.

0 votes

+1 J'ai également tendance à utiliser la construction pass-by-value-then-move depuis que j'ai commencé à utiliser C++11. Cela me met cependant un peu mal à l'aise, puisque mon code a maintenant std::move partout

75voto

Ayjay Points 1717

Dans presque tous les cas, votre sémantique devrait être soit :

bar(foo f); // want to obtain a copy of f
bar(const foo& f); // want to read f
bar(foo& f); // want to modify f

Toutes les autres signatures ne doivent être utilisées qu'avec parcimonie, et avec une bonne justification. Le compilateur va maintenant presque toujours les résoudre de la manière la plus efficace. Vous pouvez simplement continuer à écrire votre code !

3 votes

Bien que je préfère passer un pointeur si je dois modifier un argument. Je suis d'accord avec le guide de style Google pour dire que cela rend plus évident le fait que l'argument sera modifié sans avoir besoin de revérifier la signature de la fonction ( google-styleguide.googlecode.com/svn/trunk/ ).

41 votes

La raison pour laquelle je n'aime pas passer des pointeurs est que cela ajoute un état d'échec possible à mes fonctions. J'essaie d'écrire toutes mes fonctions de manière à ce qu'elles soient prouvées correctes, car cela réduit considérablement l'espace dans lequel les bogues peuvent se cacher. foo(bar& x) { x.a = 3; } est bien plus fiable (et lisible !) que foo(bar* x) {if (!x) throw std::invalid_argument("x"); x->a = 3;

22 votes

@Max Lybbert : Avec un paramètre pointeur, vous n'avez pas besoin de vérifier la signature de la fonction, mais vous devez vérifier la documentation pour savoir si vous êtes autorisé à passer des pointeurs nuls, si la fonction prendra des propriétaires, etc. IMHO, un paramètre pointeur transmet beaucoup moins d'informations qu'une référence non-const. Je suis d'accord cependant qu'il serait bien d'avoir un indice visuel à l'endroit de l'appel que l'argument peut être modifié (comme la fonction ref en C#).

10voto

Edward Brey Points 8771

Passez les paramètres par valeur si, dans le corps de la fonction, vous avez besoin d'une copie de l'objet ou si vous avez seulement besoin de déplacer l'objet. Passer par const& si vous avez seulement besoin d'un accès non-mutant à l'objet.

Exemple de copie d'objet :

void copy_antipattern(T const& t) { // (Don't do this.)
    auto copy = t;
    t.some_mutating_function();
}

void copy_pattern(T t) { // (Do this instead.)
    t.some_mutating_function();
}

Exemple de déplacement d'objet :

std::vector<T> v; 

void move_antipattern(T const& t) {
    v.push_back(t); 
}

void move_pattern(T t) {
    v.push_back(std::move(t)); 
}

Exemple d'accès non mutant :

void read_pattern(T const& t) {
    t.some_const_function();
}

Pour en savoir plus, consultez les articles de blog suivants Dave Abrahams et Xiang Fan .

6voto

adi Points 2204

Quand passe-t-on les arguments par référence ou par pointeur ?

1) Pour modifier les variables locales de la fonction appelante :

Une référence (ou un pointeur) permet à la fonction appelée de modifier une variable locale de la fonction appelante. Par exemple, considérons l'exemple de programme suivant où fun() est capable de modifier la variable locale x de main().

void fun(int &x) {
    x = 20;
}

int main() {
    int x = 10;
    fun(x);
    cout<<"New value of x is "<<x;
    return 0;
}

Sortie : La nouvelle valeur de x est 20


2) Pour faire passer des arguments de grande taille :

Si un argument est grand, le passage par référence (ou pointeur) est plus efficace car seule une adresse est réellement passée, et non l'objet entier. Par exemple, considérons la classe Employé suivante et une fonction printEmpDetails() qui imprime les détails de l'employé.

class Employee {
private:
    string name;
    string desig;

    // More attributes and operations
};

void printEmpDetails(Employee emp) {
     cout<<emp.getName();
     cout<<emp.getDesig();

    // Print more attributes
}

Le problème avec le code ci-dessus est le suivant : chaque fois que printEmpDetails() est appelé, un nouvel objet Employee est construit, ce qui implique la création d'une copie de tous les membres de données. Une meilleure implémentation serait donc de passer Employee comme une référence.

void printEmpDetails(const Employee &emp) {
     cout<<emp.getName();
     cout<<emp.getDesig();

    // Print more attributes 
}

Ce point n'est valable que pour les variables de type struct et class, car nous n'obtenons aucun avantage en termes d'efficacité pour les types de base comme int, char, etc.


3) Pour éviter le découpage des objets :

Si nous passons un objet de sous-classe à une fonction qui attend un objet de super-classe alors l'objet passé est tranché s'il est passé par valeur. Par exemple, considérons le programme suivant, il imprime "This is Pet Class".

#include <iostream>
#include<string>

using namespace std;

class Pet {
public:
    virtual string getDescription() const {
        return "This is Pet class";
    }
};

class Dog : public Pet {
public:
    virtual string getDescription() const {
        return "This is Dog class";
    }
};

void describe(Pet p) { // Slices the derived class object
    cout<<p.getDescription()<<endl;
}

int main() {
    Dog d;
    describe(d);
    return 0;
}

Sortie : Voici la Classe des animaux

Si nous utilisons le passage par référence dans le programme ci-dessus, il imprime correctement "This is Dog Class". Voir le programme modifié suivant.

#include <iostream>
#include<string>

using namespace std;

class Pet {
public:
    virtual string getDescription() const {
        return "This is Pet class";
    }
};

class Dog : public Pet {
public:
    virtual string getDescription() const {
        return "This is Dog class";
    }
};

void describe(const Pet &p) { // Doesn't slice the derived class object.
    cout<<p.getDescription()<<endl;
}

int main() {
    Dog d;
    describe(d);
    return 0;
}

Sortie : Voici la classe de chien

Ce point n'est pas non plus valable pour les types de données de base comme int, char, etc.


4) Pour réaliser le Polymorphisme à l'exécution dans une fonction

Nous pouvons rendre une fonction polymorphe en lui passant des objets comme référence (ou pointeur). Par exemple, dans le programme suivant, print() reçoit une référence à l'objet de classe de base. print() appelle la fonction de classe de base show() si l'objet de classe de base est passé, et la fonction de classe dérivée show() si l'objet de classe dérivée est passé.

#include<iostream>
using namespace std;

class base {
public:
    virtual void show() {  // Note the virtual keyword here
        cout<<"In base \n";
    }
};

class derived: public base {
public:
    void show() {
        cout<<"In derived \n";
    }
};

// Since we pass b as reference, we achieve run time polymorphism here.
void print(base &b) {
    b.show();
}

int main(void) {
    base b;
    derived d;
    print(b);
    print(d);
    return 0;
}

Sortie : En base
En dérivé

-1voto

GreenScape Points 1136

Un exemple tiré de l'expérience personnelle. Un thread possède un objet chaîne de caractères. Il est passé à une fonction qui génère un autre thread, mais à l'insu de l'appelant, la fonction a pris la chaîne en tant que const. l'appelant, la fonction a pris la chaîne en tant que const std::string& et non en tant que une copie. Le premier thread est alors sorti...

pour cela, il existe une fonction d'encapsulage

void original(const std::string & str);

void wrapper(std::string * str) // takes ownership
{
   original(*str);
   delete str;
}

vois thread_mother(...)
{
   //...
   spawn_me_thread(wrapper, new std::string("this string should be used by the thread and i don't care about it anymore"));
   //...
}

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