C++ types de vue: passer par const & ou par valeur?

Cela est apparu dans une discussion de révision de code récemment, mais sans une conclusion satisfaisante. Les types en question sont analogues au C++ string_view TS. Ce sont de simples wrappers non propriétaires autour d'un pointeur et d'une longueur, décorés avec des fonctions personnalisées:

#include <cstddef>

class foo_view {
public:
    foo_view(const char* data, std::size_t len)
        : _data(data)
        , _len(len) {
    }

    // member functions related to viewing the 'foo' pointed to by '_data'.

private:
    const char* _data;
    std::size_t _len;
};

La question s'est posée de savoir s'il existe un argument de toute façon pour préférer passer de tels types de vue (y compris les types string_view et array_view à venir) par valeur ou par référence const.

Arguments dans la faveur du passage par la valeur s'élevait à 'moins de typage', 'peut muter la copie locale si la vue a des mutations significatives', et 'probablement pas moins efficace'.

Les Arguments en faveur de la référence pass-by-const se sont élevés à 'plus idiomatique pour passer des objets par const&', et 'probablement pas moins efficace'.

Y a-t-il des considérations supplémentaires qui pourraient balancer l'argument de manière concluante d'une manière ou d'une autre pour savoir s'il est préférable de passer des types de vue idiomatiques par valeur ou par const référence.

Pour cette question, il est sûr de supposer la sémantique C++11 ou C++14, et des chaînes d'outils et des architectures cibles suffisamment modernes,etc.

41
demandé sur acm 2014-12-02 21:25:03

8 réponses

En cas de doute, passez par valeur.

Maintenant, vous ne devriez que rarement être dans le doute.

Souvent, les valeurs sont coûteuses à transmettre et donnent peu d'avantages. Parfois, vous voulez réellement une référence à une valeur éventuellement mutante stockée ailleurs. Souvent, dans le code générique, vous ne savez pas si la copie est une opération coûteuse, donc vous vous trompez du côté de not.

La raison pour laquelle vous devriez passer par valeur en cas de doute est parce que les valeurs sont plus faciles à raisonner. Une référence (même un const un) aux données externes pourraient muter au milieu d'un algorithme lorsque vous appelez un rappel de fonction ou ce que vous avez, rendant ce qui semble être une fonction simple dans un désordre complexe.

Dans ce cas, vous avez déjà une liaison de référence implicite (au contenu du conteneur que vous visualisez). Ajouter une autre liaison de référence implicite (à l'objet view qui regarde dans le conteneur) n'est pas moins mauvais car il y a déjà des complications.

Enfin, les compilateurs peuvent raisonner sur valeurs mieux que ce qu'ils peuvent sur les références aux valeurs. Si vous quittez la portée analysée localement (via un rappel de pointeur de fonction), le compilateur doit présumer que la valeur stockée dans la référence const peut avoir complètement changé (si elle ne peut pas prouver le contraire). Une valeur dans le stockage automatique sans que personne ne prenne un pointeur sur elle peut être supposée ne pas modifier de la même manière - il n'y a pas de moyen défini d'y accéder et de le changer à partir d'une portée externe, donc de telles modifications peuvent être présumées ne pas se produire.

Adoptez la simplicité lorsque vous avez la possibilité de transmettre une valeur en tant que valeur. Cela n'arrive que rarement.

29
répondu Yakk - Adam Nevraumont 2014-12-02 18:42:01

EDIT: le Code est disponible ici: https://github.com/acmorrow/stringview_param

J'ai créé un exemple de code qui semble démontrer que la valeur de passage pour les objets de type string_view entraîne un meilleur code pour les appelants et les définitions de fonctions sur au moins une plate-forme.

Tout d'abord, nous définissons une fausse classe string_view (Je n'avais pas la vraie chose à portée de main) dans string_view.h:

#pragma once

#include <string>

class string_view {
public:
    string_view()
        : _data(nullptr)
        , _len(0) {
    }

    string_view(const char* data)
        : _data(data)
        , _len(strlen(data)) {
    }

    string_view(const std::string& data)
        : _data(data.data())
        , _len(data.length()) {
    }

    const char* data() const {
        return _data;
    }

    std::size_t len() const {
        return _len;
    }

private:
    const char* _data;
    size_t _len;
};

Maintenant, permet de définir certaines fonctions qui consomment un string_view, par valeur ou par référence. Voici les signatures dans example.hpp:

#pragma once

class string_view;

void __attribute__((visibility("default"))) use_as_value(string_view view);
void __attribute__((visibility("default"))) use_as_const_ref(const string_view& view);

Les corps de ces fonctions sont définis comme suit, dans example.cpp:

#include "example.hpp"

#include <cstdio>

#include "do_something_else.hpp"
#include "string_view.hpp"

void use_as_value(string_view view) {
    printf("%ld %ld %zu\n", strchr(view.data(), 'a') - view.data(), view.len(), strlen(view.data()));
    do_something_else();
    printf("%ld %ld %zu\n", strchr(view.data(), 'a') - view.data(), view.len(), strlen(view.data()));
}

void use_as_const_ref(const string_view& view) {
    printf("%ld %ld %zu\n", strchr(view.data(), 'a') - view.data(), view.len(), strlen(view.data()));
    do_something_else();
    printf("%ld %ld %zu\n", strchr(view.data(), 'a') - view.data(), view.len(), strlen(view.data()));
}

La fonction do_something_else est ici un remplaçant pour les appels arbitraires à des fonctions que le compilateur n'a pas d'aperçu (par exemple, les fonctions d'autres objets dynamiques,etc.). La déclaration est en do_something_else.hpp:

#pragma once

void __attribute__((visibility("default"))) do_something_else();

Et la définition triviale est dans do_something_else.cpp:

#include "do_something_else.hpp"

#include <cstdio>

void do_something_else() {
    std::printf("Doing something\n");
}

Nous compilons maintenant do_something_else.rpc et de l'exemple.rpc dans des bibliothèques dynamiques. Compilateur Voici Xcode 6 clang sur OS X Yosemite 10.10.1:

clang++ -mmacosx-version-min=10.10 --stdlib=libc++ -O3 -flto -march=native -fvisibility-inlines-hidden -fvisibility=hidden --std=c++11 ./do_something_else.cpp -fPIC -shared -o libdo_something_else.dylib clang++ -mmacosx-version-min=10.10 --stdlib=libc++ -O3 -flto -march=native -fvisibility-inlines-hidden -fvisibility=hidden --std=c++11 ./example.cpp -fPIC -shared -o libexample.dylib -L. -ldo_something_else

Maintenant, nous désassemblons libexample.dylib:

> otool -tVq ./libexample.dylib
./libexample.dylib:
(__TEXT,__text) section
__Z12use_as_value11string_view:
0000000000000d80    pushq   %rbp
0000000000000d81    movq    %rsp, %rbp
0000000000000d84    pushq   %r15
0000000000000d86    pushq   %r14
0000000000000d88    pushq   %r12
0000000000000d8a    pushq   %rbx
0000000000000d8b    movq    %rsi, %r14
0000000000000d8e    movq    %rdi, %rbx
0000000000000d91    movl    $0x61, %esi
0000000000000d96    callq   0xf42                   ## symbol stub for: _strchr
0000000000000d9b    movq    %rax, %r15
0000000000000d9e    subq    %rbx, %r15
0000000000000da1    movq    %rbx, %rdi
0000000000000da4    callq   0xf48                   ## symbol stub for: _strlen
0000000000000da9    movq    %rax, %rcx
0000000000000dac    leaq    0x1d5(%rip), %r12       ## literal pool for: "%ld %ld %zu\n"
0000000000000db3    xorl    %eax, %eax
0000000000000db5    movq    %r12, %rdi
0000000000000db8    movq    %r15, %rsi
0000000000000dbb    movq    %r14, %rdx
0000000000000dbe    callq   0xf3c                   ## symbol stub for: _printf
0000000000000dc3    callq   0xf36                   ## symbol stub for: __Z17do_something_elsev
0000000000000dc8    movl    $0x61, %esi
0000000000000dcd    movq    %rbx, %rdi
0000000000000dd0    callq   0xf42                   ## symbol stub for: _strchr
0000000000000dd5    movq    %rax, %r15
0000000000000dd8    subq    %rbx, %r15
0000000000000ddb    movq    %rbx, %rdi
0000000000000dde    callq   0xf48                   ## symbol stub for: _strlen
0000000000000de3    movq    %rax, %rcx
0000000000000de6    xorl    %eax, %eax
0000000000000de8    movq    %r12, %rdi
0000000000000deb    movq    %r15, %rsi
0000000000000dee    movq    %r14, %rdx
0000000000000df1    popq    %rbx
0000000000000df2    popq    %r12
0000000000000df4    popq    %r14
0000000000000df6    popq    %r15
0000000000000df8    popq    %rbp
0000000000000df9    jmp 0xf3c                   ## symbol stub for: _printf
0000000000000dfe    nop
__Z16use_as_const_refRK11string_view:
0000000000000e00    pushq   %rbp
0000000000000e01    movq    %rsp, %rbp
0000000000000e04    pushq   %r15
0000000000000e06    pushq   %r14
0000000000000e08    pushq   %r13
0000000000000e0a    pushq   %r12
0000000000000e0c    pushq   %rbx
0000000000000e0d    pushq   %rax
0000000000000e0e    movq    %rdi, %r14
0000000000000e11    movq    (%r14), %rbx
0000000000000e14    movl    $0x61, %esi
0000000000000e19    movq    %rbx, %rdi
0000000000000e1c    callq   0xf42                   ## symbol stub for: _strchr
0000000000000e21    movq    %rax, %r15
0000000000000e24    subq    %rbx, %r15
0000000000000e27    movq    0x8(%r14), %r12
0000000000000e2b    movq    %rbx, %rdi
0000000000000e2e    callq   0xf48                   ## symbol stub for: _strlen
0000000000000e33    movq    %rax, %rcx
0000000000000e36    leaq    0x14b(%rip), %r13       ## literal pool for: "%ld %ld %zu\n"
0000000000000e3d    xorl    %eax, %eax
0000000000000e3f    movq    %r13, %rdi
0000000000000e42    movq    %r15, %rsi
0000000000000e45    movq    %r12, %rdx
0000000000000e48    callq   0xf3c                   ## symbol stub for: _printf
0000000000000e4d    callq   0xf36                   ## symbol stub for: __Z17do_something_elsev
0000000000000e52    movq    (%r14), %rbx
0000000000000e55    movl    $0x61, %esi
0000000000000e5a    movq    %rbx, %rdi
0000000000000e5d    callq   0xf42                   ## symbol stub for: _strchr
0000000000000e62    movq    %rax, %r15
0000000000000e65    subq    %rbx, %r15
0000000000000e68    movq    0x8(%r14), %r14
0000000000000e6c    movq    %rbx, %rdi
0000000000000e6f    callq   0xf48                   ## symbol stub for: _strlen
0000000000000e74    movq    %rax, %rcx
0000000000000e77    xorl    %eax, %eax
0000000000000e79    movq    %r13, %rdi
0000000000000e7c    movq    %r15, %rsi
0000000000000e7f    movq    %r14, %rdx
0000000000000e82    addq    $0x8, %rsp
0000000000000e86    popq    %rbx
0000000000000e87    popq    %r12
0000000000000e89    popq    %r13
0000000000000e8b    popq    %r14
0000000000000e8d    popq    %r15
0000000000000e8f    popq    %rbp
0000000000000e90    jmp 0xf3c                   ## symbol stub for: _printf
0000000000000e95    nopw    %cs:(%rax,%rax)

Fait intéressant, la version par valeur est plusieurs instructions plus courtes. Mais c'est seulement le corps de fonction. Qu'en est appelants?

Nous allons définir certaines fonctions qui invoquent ces deux surcharges, en transmettant un const std::string&, dans example_users.hpp:

#pragma once

#include <string>

void __attribute__((visibility("default"))) forward_to_use_as_value(const std::string& str);
void __attribute__((visibility("default"))) forward_to_use_as_const_ref(const std::string& str);

Et définissez-les dans example_users.cpp:

#include "example_users.hpp"

#include "example.hpp"
#include "string_view.hpp"

void forward_to_use_as_value(const std::string& str) {
    use_as_value(str);
}

void forward_to_use_as_const_ref(const std::string& str) {
    use_as_const_ref(str);
}

Encore une fois, nous compilons example_users.cpp pour une bibliothèque partagée:

clang++ -mmacosx-version-min=10.10 --stdlib=libc++ -O3 -flto -march=native -fvisibility-inlines-hidden -fvisibility=hidden --std=c++11 ./example_users.cpp -fPIC -shared -o libexample_users.dylib -L. -lexample

Et, encore une fois, nous regardons le code généré:

> otool -tVq ./libexample_users.dylib
./libexample_users.dylib:
(__TEXT,__text) section
__Z23forward_to_use_as_valueRKNSt3__112basic_stringIcNS_11char_traitsIcEENS_9allocatorIcEEEE:
0000000000000e70    pushq   %rbp
0000000000000e71    movq    %rsp, %rbp
0000000000000e74    movzbl  (%rdi), %esi
0000000000000e77    testb   $0x1, %sil
0000000000000e7b    je  0xe8b
0000000000000e7d    movq    0x8(%rdi), %rsi
0000000000000e81    movq    0x10(%rdi), %rdi
0000000000000e85    popq    %rbp
0000000000000e86    jmp 0xf60                   ## symbol stub for: __Z12use_as_value11string_view
0000000000000e8b    incq    %rdi
0000000000000e8e    shrq    %rsi
0000000000000e91    popq    %rbp
0000000000000e92    jmp 0xf60                   ## symbol stub for: __Z12use_as_value11string_view
0000000000000e97    nopw    (%rax,%rax)
__Z27forward_to_use_as_const_refRKNSt3__112basic_stringIcNS_11char_traitsIcEENS_9allocatorIcEEEE:
0000000000000ea0    pushq   %rbp
0000000000000ea1    movq    %rsp, %rbp
0000000000000ea4    subq    $0x10, %rsp
0000000000000ea8    movzbl  (%rdi), %eax
0000000000000eab    testb   $0x1, %al
0000000000000ead    je  0xebd
0000000000000eaf    movq    0x10(%rdi), %rax
0000000000000eb3    movq    %rax, -0x10(%rbp)
0000000000000eb7    movq    0x8(%rdi), %rax
0000000000000ebb    jmp 0xec7
0000000000000ebd    incq    %rdi
0000000000000ec0    movq    %rdi, -0x10(%rbp)
0000000000000ec4    shrq    %rax
0000000000000ec7    movq    %rax, -0x8(%rbp)
0000000000000ecb    leaq    -0x10(%rbp), %rdi
0000000000000ecf    callq   0xf66                   ## symbol stub for: __Z16use_as_const_refRK11string_view
0000000000000ed4    addq    $0x10, %rsp
0000000000000ed8    popq    %rbp
0000000000000ed9    retq
0000000000000eda    nopw    (%rax,%rax)

Et, encore une fois, la version par valeur est plusieurs instructions plus courtes.

Il me semble que, au moins par la métrique grossière du nombre d'instructions, la version par valeur produit un meilleur code pour les appelants et pour les corps de fonction générés.

Je suis bien sûr ouvert aux suggestions sur la façon d'améliorer ce test. Évidemment une la prochaine étape serait de refactoriser cela en quelque chose où je pourrais le comparer de manière significative. Je vais essayer de le faire bientôt.

Je vais poster l'exemple de code sur github avec une sorte de script de construction afin que les autres puissent tester sur leurs systèmes.

Mais sur la base de la discussion ci-dessus, et des résultats de l'inspection du code généré, ma conclusion est que pass-by-value est la voie à suivre pour les types de vue.

17
répondu acm 2014-12-03 21:20:03

En mettant de côté les questions philosophiques sur la valeur de signalisation de const & - ness par rapport à la valeur-Ness en tant que paramètres de fonction, nous pouvons jeter un oeil à certaines implications ABI sur diverses architectures.

Http://www.macieira.org/blog/2012/02/the-value-of-passing-by-value/ expose la prise de décision et les tests effectués par certains QT sur x86-64, ARMv7 hard-float, MIPS hard-float (o32) et IA-64. La plupart du temps, il vérifie si les fonctions peuvent passer diverses structures à travers les registres. Pas étonnamment, il semble que chaque plate-forme peut gérer 2 pointeurs par registre. Et étant donné que sizeof(size_t) est généralement sizeof(void*), il y a peu de raisons de croire que nous allons renverser la mémoire ici.

Nous pouvons trouver plus de bois pour le feu, en tenant compte des suggestions comme: http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2013/n3538.html . notez que const ref a quelques inconvénients, à savoir le risque d'aliasing, ce qui peut empêcher des optimisations importantes et nécessiter des pensée pour le programmeur. En L'absence de support C++ pour les restrictions de C99, le passage par la valeur peut améliorer les performances et réduire la charge cognitive.

Je suppose alors que je synthétise deux arguments en faveur de la valeur pass by:

  1. les plates-formes 32 bits n'avaient souvent pas la capacité de passer deux structures de mots par registre. Ce ne semble plus être un problème.
  2. les références const sont quantitativement et qualitativement pires que les valeurs en ce sens qu'elles peuvent alias.

Tout ce qui me conduirait à favoriser la valeur de passage pour les structures

11
répondu hanumantmk 2014-12-02 20:44:19

En plus de ce qui a déjà été dit ici en faveur du passage par valeur, les optimiseurs c++ modernes luttent avec des arguments de référence.

Lorsque le corps de l'appelé n'est pas disponible dans l'Unité de traduction (la fonction réside dans une bibliothèque partagée ou dans une autre unité de traduction et l'optimisation du temps de liaison n'est pas disponible), les choses suivantes se produisent:

  1. l'optimiseur suppose que les arguments passés par référence ou référence à const peuvent être modifiés (const fait peu importe à cause de const_cast) ou référencé par un pointeur global, ou modifié par un autre thread. Fondamentalement, les arguments passés par référence deviennent des valeurs "empoisonnées" dans le site d'appel, auxquelles l'optimiseur ne peut plus appliquer de nombreuses optimisations.
  2. dans l'appelé s'il y a plusieurs arguments de référence/pointeur du même type de base, l'optimiseur suppose qu'ils alias avec autre chose et cela exclut encore de nombreuses optimisations.

Du point de vue de l'optimiseur de le passage et le retour de la vue par valeur sont les meilleurs car cela évite le besoin d'analyse d'alias: l'appelant et l'appelé possèdent exclusivement leurs copies de valeurs afin que ces valeurs ne puissent être modifiées nulle part ailleurs.

Pour un traitement détaillé du sujet, Je ne peux pas recommander assez Chandler Carruth: optimiser les Structures émergentes de C++. Le punchline de la Conférence Est "les gens ont besoin de changer la tête au sujet de passer par la valeur... le modèle de registre de passage arguments est obsolète."

7
répondu Maxim Egorushkin 2014-12-02 23:21:38

Voici mes règles de base pour passer des variables aux fonctions:

  1. si la variable peut tenir dans le registre du processeur et ne être modifié, le passage par valeur.
  2. si la variable sera modifiée, passez par référence.
  3. si la variable est plus grande que le registre du processeur et ne être modifié, passer par référence constante.
  4. Si vous devez utiliser des pointeurs, passez par smart pointer.

J'espère que ça aide.

6
répondu Thomas Matthews 2014-12-02 18:29:54

Une valeur est Une valeur et un const référence est une référence const.

Si l'objet n'est pas immuable alors les deux sont Pas des concepts équivalents.

Oui... même un objet reçu via la référence const peut muter (ou peut même être détruit alors que vous avez encore une référence const entre vos mains). const avec une référence indique seulement ce qui peut être fait en utilisant cette référence , Il ne dit rien que l'objet référencé ne mutera pas ou ne cessera pas d'exister par d'autres moyens.

Pour voir un cas très simple dans lequel l'aliasing peut mal mordre avec du code apparemment légitime, voir cette réponse .

Vous devez utiliser une référence où la logique nécessite une référence (c'est-à-dire object identity est important). Vous devriez passer une valeur lorsque la logique nécessite juste la valeur (c'est-à-dire object identity n'est pas pertinent). Avec immutables normalement, l'identité n'est pas pertinente.

Lorsque vous utilisez une référence, des précautions particulières doivent être prises pour aliasing et problèmes de durée de vie. De l'autre côté, lorsque vous passez des valeurs, vous devriez considérer que la copie est éventuellement impliquée, donc si la classe est grande et que c'est un goulot d'étranglement sérieux pour votre programme, vous pouvez envisager de passer une référence const à la place (et vérifier les problèmes d'aliasing et de durée de vie).

À mon avis, dans ce cas spécifique (juste quelques types natifs), l'excuse pour avoir besoin d'une efficacité de passage const-reference serait assez difficile à justifier. Plus probablement tout va juste être intégré de toute façon et les références ne feront que rendre les choses plus difficiles à optimiser.

Spécifier un paramètre const T& lorsque l'appelé n'est pas intéressé par l'identité (c'est-à-dire future* changements d'état) est une erreur de conception. La seule justification pour faire cette erreur intentionnellement est lorsque l'objet est lourd et faire une copie est un problème de performance grave.

Pour les petits objets faire des copies est souvent en fait mieux à partir d'une performance point de vue car il y a une indirection de moins et le côté paranoïaque de l'optimiseur n'a pas besoin de considérer les problèmes d'aliasing. Par exemple, si vous avez F(const X& a, Y& b) et X contient un membre de type Y, l'optimiseur sera obligé de considérer la possibilité que la référence non const soit réellement liée à ce sous-objet de X.

( * ) avec "future", j'inclus à la fois après le retour de la méthode (c'est-à-dire que l'appelé stocke l'adresse de l'objet et s'en souvient) et pendant la exécution du code appelé (c'est-à-dire aliasing).

3
répondu 6502 2017-05-23 11:46:18

Comme il ne fait pas la moindre différence que celui que vous utilisez dans ce cas, cela semble juste être un débat sur les egos. Ce n'est pas quelque chose qui devrait tenir un examen de code. À moins que quelqu'un mesure la performance et découvre que ce code est critique dans le temps, ce dont je doute beaucoup.

1
répondu gnasher729 2014-12-02 18:37:57

Mon argument serait d'utiliser les deux. Préférer const&. Cela devient aussi de la documentation. Si vous l'avez déclaré comme const&, le compilateur se plaindra si vous tentez de modifier l'instance (alors que vous n'aviez pas l'intention de le faire). Si vous avez l'intention de le modifier, prenez-le par valeur. Mais de cette façon, vous communiquez explicitement aux futurs développeurs que vous avez l'intention de modifier l'instance. Et const & est "probablement pas pire" que par valeur, et potentiellement beaucoup mieux (si la construction d'un l'instance est chère, et vous n'en avez pas déjà une).

0
répondu Andre Kostur 2014-12-02 18:44:56