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;
}
它说:
对于原始类型,这相当于显而易见的实现, 而对于更复杂的类型,这个定义
- 避免在该类型定义移动构造函数时复制旧值
- 接受任何类型作为新值,利用任何转换赋值运算符
- 如果新值是临时的或移动的,则避免复制。
我选择了与atomic_exchange对称的名称,因为它们的行为 同样的,除了这个函数不是原子的。
n3746还提出了一个内置的交换运算符,如下所示:
inline C& C::operator :=: (C&& y) & { see below; return *this; }
inline C& C::operator :=: (C& y) & { return *this :=: std::move(y); }
从我收集的内容来看,这些提案希望所有这三个选项能够并存,而不是相互替换。为什么有三种不同的方式来交换对象?
答案 0 :(得分:49)
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
N3553是N3746的先前修订版,在2013年4月布里斯托尔会议的演变工作组中进行了讨论。会议纪要用std::swap(x, y)
确认“烦人的ADL问题”,但结论是交换运营商不会解决这些问题。由于向后兼容性,EWG还认为,如果接受,std::swap
和交换运营商将永远共存。 EWG在布里斯托尔决定不继续进行N3553。
2013年9月芝加哥EWG会议纪要未提及N3746。我没有参加那次会议,但是假设EWG拒绝查看N3746,因为它之前在布里斯托尔做出N3553的决定。
结论:C ++委员会目前似乎没有使用交换运营商。
更新: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&&)
的调用(假设X
存在移动成员)和两个电话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
总而言之,永远不要使用std::exchange
来执行swap
。
答案 1 :(得分:6)
简短回答:没有必要,但它很有用。
答案很长:
C ++最大的可能市场之一是科学计算和工程计算,Fortran以多种方式主导。 Fortran编程并不令人愉快,但由于它能够进行各种数值优化,因此可以产生出色的结果。这是开发表达模板背后的主要原因之一,它允许像Blitz ++这样的库开发接近Fortran级别的速度(以长编译时间和神秘错误消息为代价)。 p>
开发移动语义和表达式模板是为了加速C ++的某些领域,主要是通过消除不必要的副本和临时值。在移动语义的情况下,这极大地提高了数值计算的速度,基本上没有成本给最终用户;一旦它们得到支持并且默认移动语义被添加到对象中,数字中的许多常见用法变得更快,只需允许已存在的库停止在常见操作上执行完整副本。 由于移动语义的巨大成功,语言的其他领域,传统上由复制和交换等习语主导,正在以新的角度进行观察和标准化。 std :: array是一个这样的强度降低的例子;以前大多数标准编写者都会说“使用向量,他们会做你想做的一切,如果他们很慢就会关心”,现在调用的是更专业和特定的容器,例如static std :: array。
为什么要交换?
如果查看boost::swap,您就会理解为什么我们需要新的交换运算符:Argument Dependent Lookup很难封装和正确使用,并导致所需函数的爆炸式增长,其中提供交换成员函数的基本思想非常简单。拥有可以执行此操作的操作员,并提供可用于默认复制和交换的默认交换操作符,这是一个巨大的性能提升。
为什么呢?因为std :: swap是根据C ++ 11中的MoveConstructible和MoveAssignable定义的(以前是复制构造和复制赋值,在C ++ 98中);这需要三个移动和一个临时(比C ++ 98中所需的完整副本快得多)。这是通用的,而且非常快,但不如自定义交换快(通过在许多情况下删除临时和一次移动可以快2-3倍)。 std :: swap还取决于nothrow-move-constructible和nothrow-move-assignable的类型;可以想象一个不是,但可以在自定义交换上提供异常保证的类,从而避免未定义的行为。
ADL和std :: swap可以很好地交互,但语法有些奇怪;你添加
using std::swap;
到你的函数调用swap,并提供一个免费的朋友函数作为交换专业化。使用显式运算符替换这个奇怪的隐式ADL转角情况会更容易看到,但如上所述,它似乎在到达时已经死了。
交换是一个非常相似的野兽
通过交换使用std :: move,不再需要完整副本。通过对new_val使用通用引用,新值可以完美地转发或直接移动到它的新位置。从理论上讲,交换可以完全零拷贝运行,只需两次。
摘要
为什么有必要?因为它很快并且不会给最终用户带来任何成本,并且将C ++扩展为科学计算中Fortran的有用替代品。