web-dev-qa-db-ja.com

std :: swap vs std :: exchange vs swap演算子

std::swapの実装は次のようになります。

template <class T> void swap (T& a, T& b)
{
  T c(std::move(a)); a=std::move(b); b=std::move(c);
}
template <class T, size_t N> void swap (T (&a)[N], T (&b)[N])
{
  for (size_t i = 0; i<N; ++i) swap (a[i],b[i]);
}

実装 std::exchange n3668 は次のようになります。

 template< typename T, typename U = T >
   T exchange( T & obj, U && new_val )
   {
     T old_val = std::move(obj);
     obj = std::forward<U>(new_val);
     return old_val;
   }

それは言う:

プリミティブ型の場合、これは明らかな実装と同等ですが、より複雑な型の場合、この定義は

  • その型が移動コンストラクターを定義する場合、古い値のコピーを回避します
  • 変換代入演算子を利用して、任意の型を新しい値として受け入れます
  • 新しい値が一時的または移動されている場合は、新しい値をコピーしないようにします。

この関数はアトミックではないことを除いて、アトミックエクスチェンジとの対称性の名前を選択しました。

n3746 は、次のような組み込みのスワップ演算子も提案しています。

inline C& C::operator :=: (C&& y) &  { see below; return *this; } 
inline C& C::operator :=: (C& y)  &  { return *this :=: std::move(y); }

私が収集したものから、提案では、これら3つのオプションすべてを互いに置き換えるのではなく、並べて実行することを望んでいます。オブジェクトを交換するために3つの異なる方法が必要なのはなぜですか?

44
user1508519

std :: swap vs std :: exchange

swap(x, y)exchange(x, y)は同じものではありません。 exchange(x, y)yに新しい値を割り当てることはありません。これをy = exchange(x, y)のように使用すれば、そうすることができます。しかし、それはexchange(x, y)の主な使用例ではありません。 N3668 ステートメントが含まれます:

メリットはそれほど大きくありませんが、仕様コストもそれほど大きくありません。

exchangeの標準化に関して)。

N3668 は、2013年4月のブリストル会議でC++ 1yワーキングドラフトに投票されました。会議の議事録は、ライブラリワーキンググループでのこの関数の最適な名前に関する議論があったこと、および最終的には、正式な委員会での正式投票に反対することはありませんでした。正式投票は、それを作業草案に入れることを強く支持しましたが、全会一致ではありませんでした。

結論:exchangeはマイナーなユーティリティであり、swap(x, y)と競合せず、ユースケースがはるかに少ないです。

std :: swap vs swap operator

N355N3746 の以前のリビジョンは、2013年4月にブリストルで開催された会議のEvolutionワーキンググループで議論されました。会議の議事録はstd::swap(x, y)で「煩わしいADLの問題」を認めていますが、スワップオペレーターはこれらの問題に対処しないと結論付けています。下位互換性があるため、EWGは、受け入れられた場合、std::swapとswap演算子は永久に共存すると信じていました。 EWGはブリストルで N355 を続行しないことを決定しました。

2013年9月のシカゴEWG会議の議事録では N3746 については触れられていません。私はその会議には出席しませんでしたが、EWGは N3746 に関するブリストルでの以前の決定のために N3746 を検討することを拒否したと推測します。

結論:現在のところ、C++委員会はswap演算子を使用して前進しているようには見えません。

更新:std :: exchangeはstd :: swapよりも高速ですか?

プレビュー:いいえ。せいぜいexchangeswapと同じくらい高速です。最悪の場合、遅くなる可能性があります。

このようなテストを考えてみましょう:

using T = int;

void
test_swap(T& x, T& y)
{
    using std::swap;
    swap(x, y);
}

void
test_exchange(T& x, T& y)
{
    y = std::exchange(x, std::move(y));
}

どちらがより高速なコードを生成しますか?

Clang -O3を使用すると、どちらも同じコードを生成します(関数のマングル名を除く)。

__Z9test_swapRiS_:                      ## @_Z9test_swapRiS_
    .cfi_startproc
## BB#0:                                ## %entry
    pushq   %rbp
Ltmp0:
    .cfi_def_cfa_offset 16
Ltmp1:
    .cfi_offset %rbp, -16
    movq    %rsp, %rbp
Ltmp2:
    .cfi_def_cfa_register %rbp
    movl    (%rdi), %eax
    movl    (%rsi), %ecx
    movl    %ecx, (%rdi)
    movl    %eax, (%rsi)
    popq    %rbp
    retq
    .cfi_endproc

特殊なX関数を持たない任意のタイプswapの場合、両方のテストでX(X&&)への1つの呼び出しが生成されます(Xの移動メンバーが存在すると想定) )、2つの呼び出しX& operator=(X&&)

test_swap

__Z9test_swapR1XS0_:                    ## @_Z9test_swapR1XS0_
    .cfi_startproc
## BB#0:                                ## %entry
    pushq   %rbp
Ltmp0:
    .cfi_def_cfa_offset 16
Ltmp1:
    .cfi_offset %rbp, -16
    movq    %rsp, %rbp
Ltmp2:
    .cfi_def_cfa_register %rbp
    pushq   %r15
    pushq   %r14
    pushq   %rbx
    pushq   %rax
Ltmp3:
    .cfi_offset %rbx, -40
Ltmp4:
    .cfi_offset %r14, -32
Ltmp5:
    .cfi_offset %r15, -24
    movq    %rsi, %r14
    movq    %rdi, %rbx
    leaq    -32(%rbp), %r15
    movq    %r15, %rdi
    movq    %rbx, %rsi
    callq   __ZN1XC1EOS_
    movq    %rbx, %rdi
    movq    %r14, %rsi
    callq   __ZN1XaSEOS_
    movq    %r14, %rdi
    movq    %r15, %rsi
    callq   __ZN1XaSEOS_
    addq    $8, %rsp
    popq    %rbx
    popq    %r14
    popq    %r15
    popq    %rbp
    retq
    .cfi_endproc

test_exchange

    .globl  __Z13test_exchangeR1XS0_
    .align  4, 0x90
__Z13test_exchangeR1XS0_:               ## @_Z13test_exchangeR1XS0_
    .cfi_startproc
## BB#0:                                ## %entry
    pushq   %rbp
Ltmp6:
    .cfi_def_cfa_offset 16
Ltmp7:
    .cfi_offset %rbp, -16
    movq    %rsp, %rbp
Ltmp8:
    .cfi_def_cfa_register %rbp
    pushq   %r14
    pushq   %rbx
    subq    $16, %rsp
Ltmp9:
    .cfi_offset %rbx, -32
Ltmp10:
    .cfi_offset %r14, -24
    movq    %rsi, %r14
    movq    %rdi, %rbx
    leaq    -24(%rbp), %rdi
    movq    %rbx, %rsi
    callq   __ZN1XC1EOS_
    movq    %rbx, %rdi
    movq    %r14, %rsi
    callq   __ZN1XaSEOS_
    leaq    -32(%rbp), %rsi
    movq    %r14, %rdi
    callq   __ZN1XaSEOS_
    addq    $16, %rsp
    popq    %rbx
    popq    %r14
    popq    %rbp
    retq
    .cfi_endproc

ここでも、ほぼ同じコードです。

しかし、最適化されたswapを持つ型の場合、test_swapははるかに優れたコードを生成する可能性があります。検討してください:

using T = std::string;

(libc ++を使用)

test_swap

    .globl  __Z9test_swapRNSt3__112basic_stringIcNS_11char_traitsIcEENS_9allocatorIcEEEES6_
    .align  4, 0x90
__Z9test_swapRNSt3__112basic_stringIcNS_11char_traitsIcEENS_9allocatorIcEEEES6_: ## @_Z9test_swapRNSt3__112basic_stringIcNS_11char_traitsIcEENS_9allocatorIcEEEES6_
    .cfi_startproc
## BB#0:                                ## %entry
    pushq   %rbp
Ltmp0:
    .cfi_def_cfa_offset 16
Ltmp1:
    .cfi_offset %rbp, -16
    movq    %rsp, %rbp
Ltmp2:
    .cfi_def_cfa_register %rbp
    movq    16(%rdi), %rax
    movq    %rax, -8(%rbp)
    movq    (%rdi), %rax
    movq    8(%rdi), %rcx
    movq    %rcx, -16(%rbp)
    movq    %rax, -24(%rbp)
    movq    16(%rsi), %rax
    movq    %rax, 16(%rdi)
    movq    (%rsi), %rax
    movq    8(%rsi), %rcx
    movq    %rcx, 8(%rdi)
    movq    %rax, (%rdi)
    movq    -8(%rbp), %rax
    movq    %rax, 16(%rsi)
    movq    -24(%rbp), %rax
    movq    -16(%rbp), %rcx
    movq    %rcx, 8(%rsi)
    movq    %rax, (%rsi)
    popq    %rbp
    retq
    .cfi_endproc

test_exchange

    .globl  __Z13test_exchangeRNSt3__112basic_stringIcNS_11char_traitsIcEENS_9allocatorIcEEEES6_
    .align  4, 0x90
__Z13test_exchangeRNSt3__112basic_stringIcNS_11char_traitsIcEENS_9allocatorIcEEEES6_: ## @_Z13test_exchangeRNSt3__112basic_stringIcNS_11char_traitsIcEENS_9allocatorIcEEEES6_
Lfunc_begin0:
    .cfi_startproc
    .cfi_personality 155, ___gxx_personality_v0
    .cfi_lsda 16, Lexception0
## BB#0:                                ## %entry
    pushq   %rbp
Ltmp9:
    .cfi_def_cfa_offset 16
Ltmp10:
    .cfi_offset %rbp, -16
    movq    %rsp, %rbp
Ltmp11:
    .cfi_def_cfa_register %rbp
    pushq   %r14
    pushq   %rbx
    subq    $32, %rsp
Ltmp12:
    .cfi_offset %rbx, -32
Ltmp13:
    .cfi_offset %r14, -24
    movq    %rsi, %r14
    movq    %rdi, %rbx
    movq    16(%rbx), %rax
    movq    %rax, -32(%rbp)
    movq    (%rbx), %rax
    movq    8(%rbx), %rcx
    movq    %rcx, -40(%rbp)
    movq    %rax, -48(%rbp)
    movq    $0, 16(%rbx)
    movq    $0, 8(%rbx)
    movq    $0, (%rbx)
Ltmp3:
    xorl    %esi, %esi
                                        ## kill: RDI<def> RBX<kill>
    callq   __ZNSt3__112basic_stringIcNS_11char_traitsIcEENS_9allocatorIcEEE7reserveEm
Ltmp4:
## BB#1:                                ## %_ZNSt3__112basic_stringIcNS_11char_traitsIcEENS_9allocatorIcEEE5clearEv.exit.i.i
    movq    16(%r14), %rax
    movq    %rax, 16(%rbx)
    movq    (%r14), %rax
    movq    8(%r14), %rcx
    movq    %rcx, 8(%rbx)
    movq    %rax, (%rbx)
    movq    $0, 16(%r14)
    movq    $0, 8(%r14)
    movq    $0, (%r14)
    movw    $0, (%r14)
Ltmp6:
    xorl    %esi, %esi
    movq    %r14, %rdi
    callq   __ZNSt3__112basic_stringIcNS_11char_traitsIcEENS_9allocatorIcEEE7reserveEm
Ltmp7:
## BB#2:                                ## %_ZNSt3__112basic_stringIcNS_11char_traitsIcEENS_9allocatorIcEEEaSEOS5_.exit
    movq    -32(%rbp), %rax
    movq    %rax, 16(%r14)
    movq    -48(%rbp), %rax
    movq    -40(%rbp), %rcx
    movq    %rcx, 8(%r14)
    movq    %rax, (%r14)
    xorps   %xmm0, %xmm0
    movaps  %xmm0, -48(%rbp)
    movq    $0, -32(%rbp)
    leaq    -48(%rbp), %rdi
    callq   __ZNSt3__112basic_stringIcNS_11char_traitsIcEENS_9allocatorIcEEED1Ev
    addq    $32, %rsp
    popq    %rbx
    popq    %r14
    popq    %rbp
    retq
LBB1_3:                                 ## %terminate.lpad.i.i.i.i
Ltmp5:
    movq    %rax, %rdi
    callq   ___clang_call_terminate
LBB1_4:                                 ## %terminate.lpad.i.i.i
Ltmp8:
    movq    %rax, %rdi
    callq   ___clang_call_terminate
Lfunc_end0:
    .cfi_endproc
    .section    __TEXT,__gcc_except_tab
    .align  2
GCC_except_table1:
Lexception0:
    .byte   255                     ## @LPStart Encoding = omit
    .byte   155                     ## @TType Encoding = indirect pcrel sdata4
    .asciz  "\242\200\200"          ## @TType base offset
    .byte   3                       ## Call site Encoding = udata4
    .byte   26                      ## Call site table length
Lset0 = Ltmp3-Lfunc_begin0              ## >> Call Site 1 <<
    .long   Lset0
Lset1 = Ltmp4-Ltmp3                     ##   Call between Ltmp3 and Ltmp4
    .long   Lset1
Lset2 = Ltmp5-Lfunc_begin0              ##     jumps to Ltmp5
    .long   Lset2
    .byte   1                       ##   On action: 1
Lset3 = Ltmp6-Lfunc_begin0              ## >> Call Site 2 <<
    .long   Lset3
Lset4 = Ltmp7-Ltmp6                     ##   Call between Ltmp6 and Ltmp7
    .long   Lset4
Lset5 = Ltmp8-Lfunc_begin0              ##     jumps to Ltmp8
    .long   Lset5
    .byte   1                       ##   On action: 1
    .byte   1                       ## >> Action Record 1 <<
                                        ##   Catch TypeInfo 1
    .byte   0                       ##   No further actions
                                        ## >> Catch TypeInfos <<
    .long   0                       ## TypeInfo 1
    .align  2

したがって、要約すると、swapを実行するためにstd::exchangeを使用しないでください。

52
Howard Hinnant

短い答え:必須ではありませんが、役に立ちます。

長い答え

C++の可能な最大の市場の1つは、科学計算と工学計算であり、Fortranによって多くの点で支配されています。 Fortranは、プログラミングするのは必ずしも快適ではありませんが、さまざまな数値最適化が可能なため、優れた結果を生成します。これは、式テンプレートの開発の背後にある主要な理由の1つでした。これにより、Blitz ++のようなライブラリはFortranに近いレベルの速度を開発できました(長いコンパイル時間と不可解なエラーを犠牲にして)メッセージ)。

移動のセマンティクスと式テンプレートは、C++の特定の領域を高速化するために開発されました。移動のセマンティクスの場合、これにより数値計算の速度が大幅に向上しましたが、基本的にはエンドユーザーの負担はありません。それらがサポートされ、デフォルトの移動セマンティクスがオブジェクトに追加されると、既存のライブラリが一般的な操作でフルコピーを停止できるようにするだけで、数値の多くの一般的な使用法が速くなりました。移動セマンティクスの劇的な成功により、従来はコピーアンドスワップなどのイディオムが主だった言語の他の領域が、新たな観点から見られ、標準化されています。 std :: arrayは、そのような強度削減の1つの例です。以前のほとんどの標準的なライターが「ベクターを使用すると、必要なすべてのことを実行し、遅い場合は気にする」と言っていましたが、ここでの呼び出しは、静的std :: arrayなどのより特殊化された特定のコンテナーに対するものです。

では、なぜスワップするのですか?

boost :: swap を見ると、新しいswap演算子が必要な理由がわかります。引数に依存するルックアップは、カプセル化して正しく使用することが難しく、必要な関数が急増します。ここで、スワップメンバー関数を与えるだけの基本的な考え方は非常に簡単です。それを実行できるオペレーターがあり、デフォルトのコピーとスワップに使用できるデフォルトのスワップオペレーターを提供すると、パフォーマンスが大幅に向上します。

どうして? std :: swapは、C++ 11ではMoveConstructibleおよびMoveAssignableの観点から定義されているため(以前はC++ 98ではコピー構築とコピー割り当て)、これには3つの移動と一時的な処理が必要です(C++ 98で必要な完全なコピーよりはるかに高速)。これは一般的で非常に高速ですが、カスタムスワップほど高速ではありません(多くの場合、一時的な移動と1つの移動を削除することで、2〜3倍高速になります)。 std :: swapは、nothrow-move-constructibleおよびnothrow-move-assignableであるタイプにも依存します。そうではないが、カスタムスワップで例外を保証できるクラスを考えることは考えられるため、未定義の動作を回避します。

ADLとstd :: swapは非常にうまく相互作用できますが、構文は少し奇妙です。あなたが追加します

using std::swap;

swapを呼び出す関数に追加し、swap特殊化として無料のフレンド関数を提供します。この奇妙な暗黙のADLのコーナーケースを明示的な演算子で置き換えると、目には簡単になりますが、前述のように、到着時に死んでいるようです。

Exchangeは非常によく似た獣です

代わりにstd :: moveを使用すると、完全なコピーは不要になります。 new_valのユニバーサル参照を使用することにより、新しい値を完全に転送したり、新しい値に直接移動したりできます。理論的には、交換は完全にゼロのコピーで実行でき、2つの移動のみで実行できます。

まとめ

なぜそれが必要なのですか?それは高速であり、エンドユーザーにコストをかけず、科学計算におけるFortranの便利な代替手段としてC++を拡張するためです。

6
Alice