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.
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.
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.
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:
- 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.
- 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
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:
- 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 deconst_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. - 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."
Voici mes règles de base pour passer des variables aux fonctions:
- si la variable peut tenir dans le registre du processeur et ne être modifié, le passage par valeur.
- si la variable sera modifiée, passez par référence.
- si la variable est plus grande que le registre du processeur et ne être modifié, passer par référence constante.
- Si vous devez utiliser des pointeurs, passez par smart pointer.
J'espère que ça aide.
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).
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.
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).