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.
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.
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.
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:
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.
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:
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.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".
Voici mes règles de base pour passer des variables aux fonctions:
J'espère que cela pourra aider.
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).
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).
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.