これは最近コードレビューの議論で出てきましたが、満足のいく結論はありませんでした。問題の型は、C++ string_view TSに類似しています。これらは、いくつかのカスタム関数で装飾された、ポインターと長さの単純な非所有ラッパーです。
#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;
};
このようなビュータイプ(次のstring_viewおよびarray_viewタイプを含む)を値またはconst参照のいずれかで渡すことを好む引数があるかどうかという疑問が生じました。
値による受け渡しを支持する議論は、「型なし」、「ビューに意味のある変更がある場合にローカルコピーを変更できる」、「おそらくそれほど効率的ではない」に達しました。
Const-by-const-referenceを支持する議論は、「const&によってオブジェクトを渡す方がより慣用的」であり、「おそらくそれほど効率的ではありません」。
慣用的なビューの型を値で渡すのか、const参照で渡すのが良いかという点で、どちらかと言えば議論を揺るがす可能性がある追加の考慮事項はありますか?.
この質問では、C++ 11またはC++ 14のセマンティクス、および十分に最新のツールチェーンやターゲットアーキテクチャなどを想定するのが安全です。
疑わしい場合は、値渡ししてください。
さて、あなたはめったに疑うべきではありません。
多くの場合、値は渡すのにコストがかかり、ほとんどメリットがありません。場合によっては、実際には別の場所に格納されている、変更される可能性のある値への参照が必要になることがあります。多くの場合、一般的なコードでは、コピーが高価な操作であるかどうかがわからないため、そうでない場合はエラーになります。
疑わしいときに値で渡す必要があるのは、値の方が推論しやすいためです。外部データへの参照(const
oneでも)は、関数コールバックまたは何を持っているかをアルゴリズムの途中で変更し、単純な関数のように見えるものを複雑な混乱にレンダリングする可能性があります。
この場合、すでに暗黙的な参照バインドがあります(表示しているコンテナーのコンテンツに)。もう1つの暗黙的な参照バインドを(コンテナーを参照するビューオブジェクトに)追加すると、既に複雑なため、これも悪くありません。
最後に、コンパイラーは、値への参照についてよりも、値について推論することができます。 (関数ポインターコールバックを介して)ローカルで分析されたスコープを離れた場合、コンパイラーはconst参照に格納された値が完全に変更された可能性があると推測する必要があります(逆のことが証明できない場合)。誰もポインターを持たない自動ストレージの値は、同様の方法で変更されていないと見なすことができます。値にアクセスして外部スコープから変更するための定義された方法がないため、そのような変更は発生しないと推定できます。 。
値として値を渡す機会がある場合は、単純さを受け入れます。まれにしか起こりません。
編集:コードはここにあります: https://github.com/acmorrow/stringview_param
String_viewのようなオブジェクトの値渡しにより、呼び出し元と関数定義の両方に優れたコードが得られることを示すコード例をいくつか作成しました少なくとも1つのプラットフォーム。
最初に、string_view.h
に偽のstring_viewクラスを定義します(実物はありませんでした)。
#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;
};
次に、string_viewを使用するいくつかの関数を、値または参照によって定義してみましょう。 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);
これらの関数の本体は、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()));
}
ここにあるdo_something_else
関数は、コンパイラーが認識できない関数(たとえば、他の動的オブジェクトからの関数など)への任意の呼び出しの代用です。宣言はdo_something_else.hpp
にあります。
#pragma once
void __attribute__((visibility("default"))) do_something_else();
そして、ささいな定義はdo_something_else.cpp
にあります:
#include "do_something_else.hpp"
#include <cstdio>
void do_something_else() {
std::printf("Doing something\n");
}
次に、do_something_else.cppとexample.cppを個別の動的ライブラリにコンパイルします。ここでのコンパイラは、OS X Yosemite 10.10.1上のXCode 6 clangです。
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
次に、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)
興味深いことに、値渡しバージョンはいくつかの命令が短くなっています。しかし、それは関数本体のみです。発信者はどうですか?
これら2つのオーバーロードを呼び出すいくつかの関数を定義して、const std::string&
を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);
そして、それらを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);
}
ここでも、共有ライブラリにexample_users.cpp
をコンパイルします。
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
そして、再び、生成されたコードを確認します。
> 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)
また、値渡しバージョンでは、いくつかの命令が短くなっています。
少なくとも命令数の大まかな基準では、値渡しバージョンの方が呼び出し元と生成された関数本体の両方に対してより良いコードを生成するように思えます。
もちろん、私はこのテストを改善する方法についての提案を受け入れています。明らかに次のステップは、これをリファクタリングして、意味のあるベンチマークが可能なものにすることです。すぐにやってみます。
他の人が自分のシステムでテストできるように、サンプルコードをある種のビルドスクリプトとともにgithubに投稿します。
しかし、上記の議論、および生成されたコードを検査した結果に基づいて、私の結論は、値渡しがビュータイプに進む方法であるということです。
関数パラメーターとしてのconst&-nessとvalue-nessのシグナリング値についての哲学的な質問はさておき、さまざまなアーキテクチャーに対するいくつかのABIの影響を見てみましょう。
http://www.macieira.org/blog/2012/02/the-value-of-passing-by-value/ いくつかの意思決定とx86の一部のQT関係者によるテストのレイアウト64、ARMv7ハードフロート、MIPSハードフロート(o32)、およびIA-64。ほとんどの場合、関数がレジスタを介してさまざまな構造体を渡すことができるかどうかをチェックします。当然のことながら、各プラットフォームはレジスタによって2つのポインタを管理できるようです。そして、sizeof(size_t)が一般的にsizeof(void *)であることを考えると、ここでメモリにこぼれると信じる理由はほとんどありません。
http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2013/n3538.html のような提案を考慮して、火のためにもっと木を見つけることができます。 const refにはいくつかの欠点、つまりエイリアシングのリスクがあることに注意してください。これにより、重要な最適化が妨げられ、プログラマーに特別な配慮が必要になる場合があります。 C99の制限に対するC++サポートがない場合、値で渡すとパフォーマンスが向上し、認知負荷が低下します。
次に、値渡しを優先して2つの引数を合成しているとします。
これらすべては、整数型の16バイト未満の構造体の値渡しを優先するように私を導きます。当然のことながら、マイレージはさまざまで、パフォーマンスが問題となる場所では常にテストを行う必要がありますが、値が非常に小さい型の場合は少し良いように見えます。
ここですでに値渡しを支持することで支持されていることに加えて、最新のC++オプティマイザーは参照引数と格闘します。
呼び出し先の本体が翻訳単位で使用できない場合(関数が共有ライブラリまたは別の翻訳単位にあり、リンク時の最適化が使用できない場合)、次のことが起こります。
const_cast
であるため、const
は問題ではない)、グローバルポインターによって参照される、または別のスレッドによって変更されると想定します。基本的に、参照によって渡される引数は呼び出しサイトで「毒された」値になり、オプティマイザはこれ以上多くの最適化を適用できません。オプティマイザの観点からは、値による受け渡しと値による戻りが最良です。これにより、エイリアス分析の必要がなくなります。呼び出し元と呼び出し先が値のコピーを排他的に所有するため、これらの値を他の場所から変更できません。
主題の詳細な扱いについて、私は十分にお勧めすることはできません Chandler Carruth:C++の創発的構造の最適化 。話の要点は、「人々は値渡しについて頭を変える必要がある...引数渡しのレジスターモデルは時代遅れです」です。
変数を関数に渡すための経験則は次のとおりです。
お役に立てば幸いです。
値は値であり、const参照はconst参照です。
オブジェクトが不変でない場合、2つは[〜#〜] not [〜#〜]の同等の概念です。
はい... const
参照を介して受け取ったオブジェクトでさえも変化する可能性があります(または、const参照がまだ手元にある間に破棄することもできます)。参照付きのconst
は、その参照を使用して何ができるかのみを示します。参照されたオブジェクトが変更されないこと、または他の方法で存在しなくなることはありません。
明らかに正当なコードでエイリアシングがひどく噛み付く非常に単純なケースを見るには、 この答え を参照してください。
ロジックが参照を必要とする場合は参照を使用する必要があります(つまり、オブジェクトidentityが重要です)。ロジックが値のみを必要とする場合は値を渡す必要があります(つまり、オブジェクトidentityは無関係です)。イミュータブルでは通常、アイデンティティは無関係です。
リファレンスを使用するときは、エイリアシングと寿命の問題に特別な注意を払う必要があります。一方、値を渡すときは、コピーが含まれる可能性があることを考慮する必要があります。したがって、クラスが大きく、これがプログラムにとって重大なボトルネックである可能性がある場合は、代わりにconst参照を渡すことを検討してください(エイリアスと寿命の問題を再確認してください)。 。
私の意見では、この特定のケース(ネイティブ型のカップルのみ)では、const-referenceの受け渡し効率を必要とする言い訳は正当化するのが非常に難しいでしょう。ほとんどの場合、とにかくすべてがインライン化され、参照は最適化を困難にするだけです。
呼び出し先がアイデンティティに興味がないときに_const T&
_パラメーターを指定する(つまり、将来* 状態変化)は設計エラーです。このエラーを意図的に作成する正当な理由は、オブジェクトが重い場合であり、コピーを作成することがパフォーマンス上の重大な問題である場合です。
小さなオブジェクトの場合、コピーを作成することは、多くの場合、パフォーマンスの観点からは実際には良いので、間接指定が1つ少なく、オプティマイザの偏執的な側は考慮する必要がありません。エイリアシングの問題。たとえば、F(const X& a, Y& b)
があり、X
にタイプY
のメンバーが含まれている場合、オプティマイザは非const参照が実際にバインドされている可能性を考慮するように強制されますX
のサブオブジェクト。
(*)「将来」には、メソッドから戻った後(つまり、呼び出し先がオブジェクトのアドレスを格納して記憶している)と呼び出し先コードの実行中(つまり、エイリアス)の両方が含まれます。
私の主張は、両方を使用することです。 const&を優先します。また、ドキュメントにもなります。これをconst&として宣言した場合、インスタンスを変更しようとすると(意図しない場合)、コンパイラーは文句を言うでしょう。変更する予定がある場合は、値を指定してください。ただし、この方法では、インスタンスを変更するつもりであることを将来の開発者に明示的に伝えます。また、const&は値よりも「おそらく悪くない」可能性があり、潜在的にはるかに優れています(インスタンスの構築にコストがかかり、まだインスタンスがない場合)。
この場合、どちらを使用してもほとんど違いはないので、これは自我についての議論のようです。これは、コードレビューを保留するものではありません。誰かがパフォーマンスを測定し、このコードがタイムクリティカルであることを理解しない限り、私は非常に疑っています。