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つの異なる方法が必要なのはなぜですか?
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
N355 、 N3746 の以前のリビジョンは、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よりも高速ですか?
プレビュー:いいえ。せいぜいexchange
はswap
と同じくらい高速です。最悪の場合、遅くなる可能性があります。
このようなテストを考えてみましょう:
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
を使用しないでください。
短い答え:必須ではありませんが、役に立ちます。
長い答え:
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++を拡張するためです。