web-dev-qa-db-fra.com

Types de vues C ++: passer par const ou par valeur?

Cela est apparu lors d'une discussion sur la révision du code récemment, mais sans conclusion satisfaisante. Les types en question sont analogues au TS string_view C++. Ce sont de simples wrappers sans propriétaire autour d'un pointeur et d'une longueur, décorés de quelques 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 y avait un argument dans les deux cas pour préférer passer ces types de vue (y compris les types string_view et array_view à venir) par valeur ou par référence const.

Les arguments en faveur du passage par valeur équivalaient à "moins de frappe", "peuvent muter la copie locale si la vue présente des mutations significatives" et "probablement pas moins efficaces".

Les arguments en faveur de la référence passe-par-const étaient "plus idiomatiques pour passer des objets par const &", et "probablement pas moins efficaces".

Y a-t-il des considérations supplémentaires qui pourraient faire basculer l'argument de manière concluante dans un sens ou dans l'autre en termes de savoir s'il est préférable de passer les types de vues idiomatiques par valeur ou par référence const.

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.

55
acm

En cas de doute, passez par valeur.

Maintenant, vous ne devriez que rarement douter.

Souvent, les valeurs sont chères à passer et donnent peu d'avantages. Parfois, vous voulez en fait une référence à une valeur éventuellement en mutation stockée ailleurs. Souvent, dans le code générique, vous ne savez pas si la copie est une opération coûteuse, vous vous trompez donc.

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

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

Enfin, les compilateurs peuvent mieux raisonner sur les valeurs que 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 avec personne ne prenant un pointeur vers elle peut être supposée ne pas modifier d'une manière similaire - il n'y a pas de moyen défini pour y accéder et la changer à partir d'une portée externe, de sorte que 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.

30

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 similaires à string_view entraîne un meilleur code pour les appelants et les définitions de fonction 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, soit par valeur, soit 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()));
}

Le do_something_else function is here est un remplaçant pour les appels arbitraires à des fonctions sur lesquelles le compilateur n'a pas d'informations (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.cpp et example.cpp dans des bibliothèques dynamiques individuelles. Le compilateur est ici 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émontons 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 ce ne sont que les organes de fonction. Et les appelants?

Nous allons définir quelques 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 vers 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, que la version par valeur produit un meilleur code pour les appelants et pour les corps de fonctions générés.

Je suis bien sûr ouvert aux suggestions pour améliorer ce test. De toute évidence, une prochaine étape serait de refaçonner cela en quelque chose où je pourrais le comparer de manière significative. J'essaierai de le faire bientôt.

Je posterai l'exemple de code sur github avec une sorte de script de construction pour que d'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 la valeur de passage est la voie à suivre pour les types de vue.

18
acm

En mettant de côté les questions philosophiques sur la valeur de signalisation de la constance et de la valeur en tant que paramètres de fonction, nous pouvons jeter un coup d'œil à certaines implications ABI sur diverses architectures.

http://www.macieira.org/blog/2012/02/the-value-of-passing-by-value/ présente la prise de décision et les tests effectués par certaines personnes QT sur x86- 64, flotteur dur ARMv7, flotteur dur MIPS (o32) et IA-64. Généralement, il vérifie si les fonctions peuvent passer diverses structures à travers les registres. Sans surprise, il apparaît que chaque plateforme 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 déverser dans la mémoire ici.

Nous pouvons trouver plus de bois pour le feu, en considérant 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'alias, qui peut empêcher des optimisations importantes et nécessiter une réflexion supplémentaire pour le programmeur. En l'absence de prise en charge C++ pour la restriction 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 du passage par valeur:

  1. Les plates-formes 32 bits n'avaient souvent pas la capacité de passer deux structures Word par registre. Cela ne semble plus être un problème.
  2. les références const sont quantitativement et qualitativement pires que les valeurs dans la mesure où elles peuvent être alias.

Tout cela me conduirait à privilégier le passage par valeur pour les structures <16 octets de types intégraux. Évidemment, votre kilométrage peut varier et les tests doivent toujours être effectués lorsque les performances sont un problème, mais les valeurs semblent un peu plus agréables pour les très petits types.

12
hanumantmk

En plus de ce qui a déjà été dit ici en faveur du passage par la valeur, les optimiseurs C++ modernes ont du mal avec les 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 événements suivants 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 n'a pas d'importance à cause de const_cast) ou référencé par un pointeur global, ou modifié par un autre thread. Fondamentalement, les arguments transmis 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 existe plusieurs arguments de référence/pointeur du même type de base, l'optimiseur suppose qu'ils aliasent avec autre chose et cela exclut à nouveau de nombreuses optimisations.

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

Pour un traitement détaillé du sujet, je ne saurais trop en recommander Chandler Carruth: Optimizing the Emergent Structures of C++ . Le point fort de la conférence est "les gens doivent changer d'avis sur le passage par valeur ... le modèle de registre des arguments de passage est obsolète".

9
Maxim Egorushkin

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 sera pas modifiée, passez 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 sera pas modifiée, passez par référence constante.
  4. Si vous devez utiliser des pointeurs, passez par un pointeur intelligent.

J'espère que cela pourra aider.

6
Thomas Matthews

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

Si l'objet n'est pas immuable, les deux sont [~ # ~] pas [~ # ~] concepts équivalents.

Oui ... même un objet reçu via const référence peut muter (ou peut même être détruit alors que vous avez toujours 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 sur le fait que l'objet référencé ne mute 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 que l'identité de l'objet est importante). Vous devez transmettre une valeur lorsque la logique requiert uniquement la valeur (c'est-à-dire que l'identité de l'objet n'est pas pertinente). Avec les immuables, l'identité n'a généralement pas d'importance.

Lorsque vous utilisez une référence, une attention particulière doit être portée aux problèmes d'alias et de durée de vie. D'un autre côté, lorsque vous transmettez des valeurs, vous devez considérer que la copie est peut-être impliquée, donc si la classe est grande et que cela constitue un goulot d'étranglement sérieux pour votre programme, vous pouvez envisager de passer une référence const à la place (et revérifiez les problèmes d'alias et de durée de vie) .

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

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

Pour les petits objets, la copie est souvent meilleure du point de vue des performances car il y a une indirection de moins et le côté paranoïaque de l'optimiseur n'a pas besoin de prendre en compte 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 forcé de considérer la possibilité que la référence non-const y soit réellement liée. 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 l'exécution du code de l'appelé (c'est-à-dire l'aliasing).

3
6502

Mon argument serait d'utiliser les deux. Préférez const &. Cela peut également être de la documentation. Si vous l'avez déclaré en tant que const &, le compilateur se plaindra si vous essayez de modifier l'instance (alors que vous n'en aviez pas l'intention). 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 & n'est "probablement pas pire" qu'en valeur, et potentiellement beaucoup mieux (si la construction d'une instance est coûteuse et que vous n'en avez pas déjà une).

0
Andre Kostur

Comme cela ne fait pas la moindre différence avec 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 retarder la révision du code. À moins que quelqu'un ne mesure les performances et ne comprenne que ce code est critique en temps, ce dont je doute beaucoup.

0
gnasher729