std :: swap vs std :: exchange vs swap operator

43 c++ swap c++14

的实现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]);
}
Run Code Online (Sandbox Code Playgroud)

实现std::exchangen3668可能如下所示:

 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;
   }
Run Code Online (Sandbox Code Playgroud)

它说:

对于原始类型,这相当于明显的实现,而对于更复杂的类型,这个定义

  • 当该类型定义移动构造函数时,避免复制旧值
  • 接受任何类型作为新值,利用任何转换赋值运算符
  • 如果它是临时的或移动的,则避免复制新值.

我选择了与atomic_exchange对称的名称,因为它们的行为相同,除了这个函数不是原子的.

n3746还提出了一个内置的交换运算符,如下所示:

inline C& C::operator :=: (C&& y) &  { see below; return *this; } 
inline C& C::operator :=: (C& y)  &  { return *this :=: std::move(y); }
Run Code Online (Sandbox Code Playgroud)

根据我的收集,提案希望所有这三个选项并存,而不是互相替换.为什么有三种不同的方式来交换对象?

How*_*ant 52

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运算符

N3553,以前的修订N3746,在演进工作组在布里斯托尔2013年4月会议上讨论.会议纪要承认"恼人的ADL问题" std::swap(x, y),但结论是交换运营商不会解决这些问题.由于向后兼容性,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));
}
Run Code Online (Sandbox Code Playgroud)

哪个生成更快的代码?

使用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
Run Code Online (Sandbox Code Playgroud)

对于某些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
Run Code Online (Sandbox Code Playgroud)

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
Run Code Online (Sandbox Code Playgroud)

再次几乎相同的代码.

但对于具有优化的类型swap,test_swap很可能会产生更优越的代码.考虑:

using T = std::string;
Run Code Online (Sandbox Code Playgroud)

(使用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
Run Code Online (Sandbox Code Playgroud)

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
Run Code Online (Sandbox Code Playgroud)

总而言之,永远不要std::exchange用来执行swap.


Ali*_*ice 6

简短回答:没有必要,但它很有用.

答案很长:

C++最大的可能市场之一是科学计算和工程计算,Fortran在很多方面都占主导地位.Fortran编程并不令人愉快,但由于它能够进行各种数值优化,因此可以产生出色的结果.这是开发表达式模板背后的主要原因之一,它允许像Blitz ++这样的库开发近Fortran级别的速度(以长编译时间和神秘错误消息为代价).

开发移动语义和表达式模板是为了加速C++的某些领域,主要是通过消除不必要的副本和临时值.在移动语义的情况下,这极大地提高了数值计算的速度,基本上没有成本给最终用户; 一旦它们得到支持并且默认移动语义被添加到对象中,数字中的许多常见用法变得更快,只需允许已存在的库停止在常见操作上执行完整副本.由于移动语义的巨大成功,语言的其他领域,传统上由复制和交换等习语主导,正在以新的角度进行观察和标准化.std :: array是一个这样的强度降低的例子; 以前大多数标准编写者都会说"使用向量,他们会做你想做的一切,如果他们很慢就会关心",现在调用的是更专业和特定的容器,例如static std :: array.

为什么要互换呢?

如果你看一下 boost :: swap,你就会明白为什么我们需要新的交换运算符:Argument Dependent Lookup很难封装和正确使用,并导致所需函数的爆炸性增长,其中作为just的基本思想提供交换成员函数非常简单.拥有一个可以执行此操作的操作员,并提供一个默认的交换操作符,然后可以将其用于默认的复制和交换,这是一个巨大的性能提升.

为什么?因为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;
Run Code Online (Sandbox Code Playgroud)

你的函数调用swap,并提供一个免费的朋友函数作为交换专业化.使用显式运算符替换这个奇怪的隐式ADL转角情况会更容易看到,但如上所述,它似乎在到达时已经死了.

交换是一个非常相似的野兽

通过使用std :: move作为交换,不再需要完整副本.通过对new_val使用通用引用,新值可以完美地转发或直接移动到它的新位置.从理论上讲,交换可以运行绝对零拷贝,只需两次.

综上所述

为什么有必要?因为它很快并且不会给最终用户带来任何成本,并且将C++扩展为科学计算中Fortran的有用替代品.

  • `[...] std :: swap是根据复制构造和复制赋值定义的[...]`这是不正确的.`std :: swap`只需要它的参数是`MoveConstructible`和`MoveAssignable`(20.2.2).由于它也适用于不可复制的类型,因此无法根据复制构造和复制分配进行定义.这也意味着你最终会有三个动作(一个动作构造和两个动作分配),正如OP在问题的文本中所暗示的那样,而不是"两个副本和一个动作". (2认同)
  • 我理解C++ 03中的工作方式有所不同,但我相信OP专注于最新版本的C++(另请参阅C++ 1y标签).此外,由于您正在讨论该段落中的移动语义,因此上下文是(至少)C++ 11中的一个 - 这使得"两个副本和一个移动"句子不正确.对于没有移动构造函数的类型,这些移动会衰减以进行复制.但是,这并不意味着`std :: swap`在复制构造和复制赋值方面是(仍)定义的. (2认同)