Le pass-by-value est-il une valeur par défaut raisonnable dans C ++ 11?

142

Dans le C ++ traditionnel, le passage par valeur dans les fonctions et les méthodes est lent pour les objets volumineux 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 qui introduit toutes sortes de questions compliquées autour de la propriété et en particulier de la gestion de la mémoire (dans le cas où l'objet est alloué en tas)

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

Alors, cela signifie-t-il que la valeur par défaut devrait être de passer par valeur pour les instances de types tels que std::vectoret std::string? Qu'en est-il des objets personnalisés? Quelle est la nouvelle meilleure pratique?

Derek Thurn
la source
22
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 raté quelque chose?
iammilind le
1
@iammilind: Un exemple d'expérience personnelle. Un thread a un objet chaîne. Il est passé à une fonction qui génère un autre thread, mais inconnu de l'appelant, la fonction a pris la chaîne comme const std::string&une copie et non une copie. Le premier fil est ensuite sorti ...
Zan Lynx
12
@ZanLynx: Cela ressemble à une fonction qui n'a clairement jamais été conçue pour être appelée en tant que fonction de thread.
Nicol Bolas
5
En accord avec iammilind, je ne vois aucun problème. Le passage par référence const doit être votre valeur par défaut pour les objets "grands" et par valeur pour les objets plus petits. Je mettrais la limite entre les grands et les petits à environ 16 octets (ou 4 pointeurs sur un système 32 bits).
JN du
3
Herb Sutter est de retour aux sources! La présentation de Essentials of Modern C ++ à CppCon est entrée dans les détails à ce sujet. Vidéo ici .
Chris Drew

Réponses:

138

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

Recommandation: ne copiez pas vos arguments de fonction. Au lieu de cela, transmettez-les par valeur et laissez le compilateur faire la copie.

Dans le code, cela signifie ne pas faire ceci:

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

mais faites ceci:

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

qui a l'avantage que l'appelant peut utiliser foocomme ceci:

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é. Vous auriez besoin de deux surcharges pour faire la même chose avec les références, void foo(T const&);et void foo(T&&);.

Dans cet esprit, j'ai maintenant écrit mes précieux constructeurs en tant que tels:

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

Sinon, passer par référence à conststill est raisonnable.

Luc Danton
la source
29
+1, surtout pour le dernier bit :) Il ne faut pas oublier que les Constructeurs de Déplacement ne peuvent être invoqués que si l'objet à partir duquel se déplacer n'est pas censé rester inchangé par la suite: SomeProperty p; for (auto x: vec) { x.foo(p); }ne rentre pas, par exemple. De plus, les constructeurs de mouvements ont un coût (plus l'objet est grand, plus le coût est élevé) alors qu'ils const&sont essentiellement gratuits.
Matthieu M.
25
@MatthieuM. Mais il est important de savoir ce que signifie réellement «plus l'objet est grand, plus le coût» du déplacement est élevé: «plus grand» signifie en fait «plus il y a de variables membres». Par exemple, déplacer un std::vectoravec un million d'éléments coûte le même prix qu'en déplacer un avec cinq éléments puisque seul le pointeur vers le tableau sur le tas est déplacé, pas tous les objets du vecteur. Ce n'est donc pas vraiment un gros problème.
Lucas
+1 J'ai aussi tendance à utiliser la construction pass-by-value-then-move depuis que j'ai commencé à utiliser C ++ 11. Cela me rend un peu mal à l'aise cependant, car mon code est maintenant std::movepartout ..
stijn
1
Il y a un risque avec const&, qui m'a fait trébucher à plusieurs reprises. void foo(const T&); int main() { S s; foo(s); }. Cela peut compiler, même si les types sont différents, s'il existe un constructeur T qui prend un S comme argument. Cela peut être lent, car un grand objet T peut être construit. Vous pensez peut-être que vous passez une référence sans copier, mais vous pouvez l'être. Voir cette réponse à une question que j'ai demandée plus. Fondamentalement, &se lie généralement uniquement aux valeurs l, mais il existe une exception pour rvalue. Il existe des alternatives.
Aaron McDaid
1
@AaronMcDaid C'est une vieille nouvelle, dans le sens où vous deviez toujours être conscient même avant C ++ 11. Et rien n'a beaucoup changé à cet égard.
Luc Danton
71

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 les travaillera désormais à peu près toujours de la manière la plus efficace. Vous pouvez simplement continuer à écrire votre code!

Ayjay
la source
2
Bien que je préfère passer un pointeur si je vais modifier un argument. Je suis d'accord avec le guide de style de Google que cela rend plus évident que l'argument sera modifié sans avoir besoin de revérifier la signature de la fonction ( google-styleguide.googlecode.com/svn/trunk/… ).
Max Lybbert
40
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 pour qu'elles soient prouvées correctes, car cela réduit considérablement l'espace pour les bogues pour se cacher. foo(bar& x) { x.a = 3; }Est beaucoup plus fiable (et lisible!) foo(bar* x) {if (!x) throw std::invalid_argument("x"); x->a = 3;
Que
22
@Max Lybbert: Avec un paramètre de 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 possession, etc. IMHO, un paramètre de pointeur transmet beaucoup moins d'informations qu'une référence non const. Je conviens cependant qu'il serait bien d'avoir un indice visuel sur le site d'appel que l'argument peut être modifié (comme le refmot - clé en C #).
Luc Touraille
En ce qui concerne le passage par valeur et en s'appuyant sur la sémantique du mouvement, j'estime que ces trois choix expliquent mieux l'utilisation prévue du paramètre. Ce sont les directives que je suis toujours aussi.
Trevor Hickey
1
@AaronMcDaid is shared_ptr intended to never be null? Much as (I think) unique_ptr is?Ces deux hypothèses sont incorrectes. unique_ptret shared_ptrpeut contenir des nullptrvaleurs nulles / . Si vous ne voulez pas vous soucier des valeurs nulles, vous devez utiliser des références, car elles ne peuvent jamais être nulles. Vous n'aurez pas non plus à taper ->, ce que vous trouvez ennuyeux :)
Julian
10

Passez les paramètres par valeur si, à l'intérieur du corps de la fonction, vous avez besoin d'une copie de l'objet ou que vous avez seulement besoin de déplacer l'objet. Passez const&si vous n'avez besoin que 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 sans mutation:

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

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

Edward Brey
la source
0

La signature d'une fonction doit refléter son utilisation prévue. La lisibilité est importante, également pour l'optimiseur.

C'est la meilleure condition préalable pour qu'un optimiseur crée le code le plus rapide - en théorie du moins et sinon en réalité, dans quelques années la réalité.

Les considérations de performances sont très souvent surestimées dans le contexte du passage de paramètres. La transmission parfaite en est un exemple. Les fonctions comme emplace_backsont pour la plupart très courtes et intégrées de toute façon.

Patrick Fromberg
la source