web-dev-qa-db-ja.com

C ++ビュータイプ:constまたは値渡し?

これは最近コードレビューの議論で出てきましたが、満足のいく結論はありませんでした。問題の型は、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のセマンティクス、および十分に最新のツールチェーンやターゲットアーキテクチャなどを想定するのが安全です。

55
acm

疑わしい場合は、値渡ししてください。

さて、あなたはめったに疑うべきではありません。

多くの場合、値は渡すのにコストがかかり、ほとんどメリットがありません。場合によっては、実際には別の場所に格納されている、変更される可能性のある値への参照が必要になることがあります。多くの場合、一般的なコードでは、コピーが高価な操作であるかどうかがわからないため、そうでない場合はエラーになります。

疑わしいときに値で渡す必要があるのは、値の方が推論しやすいためです。外部データへの参照(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に投稿します。

しかし、上記の議論、および生成されたコードを検査した結果に基づいて、私の結論は、値渡しがビュータイプに進む方法であるということです。

18
acm

関数パラメーターとしての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つの引数を合成しているとします。

  1. 32ビットプラットフォームでは、2つのWord構造体をレジスタで渡すことができないことがよくありました。これはもはや問題ではないようです。
  2. const参照は、エイリアスすることができるという点で、値よりも量的および質的に劣ります。

これらすべては、整数型の16バイト未満の構造体の値渡しを優先するように私を導きます。当然のことながら、マイレージはさまざまで、パフォーマンスが問題となる場所では常にテストを行う必要がありますが、値が非常に小さい型の場合は少し良いように見えます。

12
hanumantmk

ここですでに値渡しを支持することで支持されていることに加えて、最新のC++オプティマイザーは参照引数と格闘します。

呼び出し先の本体が翻訳単位で使用できない場合(関数が共有ライブラリまたは別の翻訳単位にあり、リンク時の最適化が使用できない場合)、次のことが起こります。

  1. オプティマイザーは、参照によって渡される引数またはconstへの参照が変更可能(const_castであるため、constは問題ではない)、グローバルポインターによって参照される、または別のスレッドによって変更されると想定します。基本的に、参照によって渡される引数は呼び出しサイトで「毒された」値になり、オプティマイザはこれ以上多くの最適化を適用できません。
  2. 呼び出し先で、同じ基本型の参照/ポインター引数が複数ある場合、オプティマイザーはそれらが別の別名を持つと想定し、これにより多くの最適化が妨げられます。

オプティマイザの観点からは、値による受け渡しと値による戻りが最良です。これにより、エイリアス分析の必要がなくなります。呼び出し元と呼び出し先が値のコピーを排他的に所有するため、これらの値を他の場所から変更できません。

主題の詳細な扱いについて、私は十分にお勧めすることはできません Chandler Carruth:C++の創発的構造の最適化 。話の要点は、「人々は値渡しについて頭を変える必要がある...引数渡しのレジスターモデルは時代遅れです」です。

9

変数を関数に渡すための経験則は次のとおりです。

  1. 変数がプロセッサのレジスタに収まり、変更されない場合は、値で渡します。
  2. 変数が変更される場合は、参照渡しします。
  3. 変数がプロセッサのレジスタより大きく、変更されない場合は、定数参照で渡します。
  4. ポインタを使用する必要がある場合は、スマートポインタを渡します。

お役に立てば幸いです。

6
Thomas Matthews

値は値であり、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のサブオブジェクト。

(*)「将来」には、メソッドから戻った後(つまり、呼び出し先がオブジェクトのアドレスを格納して記憶している)と呼び出し先コードの実行中(つまり、エイリアス)の両方が含まれます。

3
6502

私の主張は、両方を使用することです。 const&を優先します。また、ドキュメントにもなります。これをconst&として宣言した場合、インスタンスを変更しようとすると(意図しない場合)、コンパイラーは文句を言うでしょう。変更する予定がある場合は、値を指定してください。ただし、この方法では、インスタンスを変更するつもりであることを将来の開発者に明示的に伝えます。また、const&は値よりも「おそらく悪くない」可能性があり、潜在的にはるかに優れています(インスタンスの構築にコストがかかり、まだインスタンスがない場合)。

0
Andre Kostur

この場合、どちらを使用してもほとんど違いはないので、これは自我についての議論のようです。これは、コードレビューを保留するものではありません。誰かがパフォーマンスを測定し、このコードがタイムクリティカルであることを理解しない限り、私は非常に疑っています。

0
gnasher729