web-dev-qa-db-fra.com

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

Une implémentation de std::swap pourrait ressembler à ceci:

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]);
}

Une implémentation std::exchange n3668 pourrait ressembler à ceci:

 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;
   }

Ça dit:

Pour les types primitifs, cela équivaut à l'implémentation évidente, tandis que pour les types plus complexes, cette définition

  • Évite de copier l'ancienne valeur lorsque ce type définit un constructeur de déplacement
  • Accepte tout type comme nouvelle valeur, en profitant de tout opérateur d'affectation de conversion
  • Évite de copier la nouvelle valeur si elle est temporaire ou déplacée.

J'ai choisi le nom de symétrie avec atomic_exchange, car ils se comportent de la même façon, sauf que cette fonction n'est pas atomique.

n3746 propose également un opérateur de swap intégré qui ressemble à ceci:

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

D'après ce que je comprends, les propositions voudraient que ces trois options cohabitent plutôt que se remplacent. Pourquoi est-il nécessaire d'avoir trois façons différentes d'échanger des objets?

44
user1508519

std :: swap vs std :: exchange

swap(x, y) et exchange(x, y) ne sont pas la même chose. exchange(x, y) n'affecte jamais une nouvelle valeur à y. Vous pouvez le faire si vous l'utilisez comme ceci: y = exchange(x, y). Mais ce n'est pas le cas d'utilisation principal de exchange(x, y). N3668 inclut la déclaration:

L'avantage n'est pas énorme, mais le coût de spécification non plus.

(en ce qui concerne la standardisation de exchange).

N3668 a été voté dans le projet de travail C++ 1y lors de la réunion de Bristol, avril 2013. Le procès-verbal de la réunion indique qu'il y a eu une discussion sur le meilleur nom pour cette fonction au sein du Library Working Group, et que finalement, il n'y avait aucune objection à le soumettre à un vote formel en comité plénier. Le vote formel était fortement en faveur de sa mise dans le projet de travail, mais pas à l'unanimité.

Conclusion: exchange est un utilitaire mineur, ne rivalise pas avec swap(x, y), et a beaucoup moins de cas d'utilisation.

std :: swap vs opérateur de swap

N355 , une précédente révision de N3746 , a été discutée au sein du groupe de travail sur l'évolution lors de la réunion d'avril 2013 à Bristol. Le procès-verbal de la réunion reconnaît les "problèmes ADL ennuyeux" avec std::swap(x, y), mais conclut qu'un opérateur de swap ne résoudrait pas ces problèmes. En raison de la rétrocompatibilité, l'EWG pensait également que s'il était accepté, std::swap Et l'opérateur de swap coexisteraient pour toujours. L'EWG a décidé à Bristol de ne pas poursuivre N355 .

Le procès-verbal de la réunion de septembre 2013 du Chicago EWG ne fait aucune mention de N3746 . Je n'étais pas présent à cette réunion, mais je suppose que le GTE a refusé d'examiner N3746 en raison de sa décision précédente à Bristol le N355 .

Conclusion: le comité C++ ne semble pas aller de l'avant avec un opérateur de swap pour le moment.

Mise à jour: std :: exchange peut-il être plus rapide que std :: swap?

Aperçu: Non. Au mieux, exchange sera aussi rapide que swap. Au pire, cela peut être plus lent.

Considérez un test comme celui-ci:

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));
}

Qui génère un code plus rapide?

En utilisant clang -O3, ils génèrent tous les deux du code identique (à l'exception des noms mutilés des fonctions):

__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

Pour certains types arbitraires X, qui n'ont pas de fonction spécialisée swap, les deux tests généreront un appel à X(X&&) (en supposant que des membres de déplacement existent pour X) et deux appels 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

Encore une fois presque le même code.

Mais pour les types qui ont un swap optimisé, test_swap Est susceptible de générer un code bien supérieur. Considérer:

using T = std::string;

(en utilisant 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

Donc en résumé, n'utilisez jamais std::exchange Pour effectuer un swap.

52
Howard Hinnant

Réponse courte : ce n'est pas nécessaire, mais c'est utile.

Réponse longue :

L'un des plus grands marchés possibles pour le C++ est le calcul scientifique et le calcul technique, qui est dominé à bien des égards par Fortran. Fortran n'est pas exactement agréable à programmer, mais génère des résultats supérieurs en raison des diverses optimisations numériques dont il est capable. C'est l'une des principales raisons du développement de modèles d'expression, qui a permis à des bibliothèques comme Blitz ++ de développer des niveaux de vitesse proches de Fortran (au prix de longs temps de compilation et de messages d'erreur cryptiques).

La sémantique de mouvement et les modèles d'expression ont été développés pour accélérer certains domaines du C++, principalement en éliminant les copies inutiles et les valeurs temporaires. Dans le cas de la sémantique des mouvements, cela a considérablement augmenté la vitesse des calculs numériques sans aucun coût pour l'utilisateur final; une fois qu'ils ont été pris en charge et que la sémantique de déplacement par défaut a été ajoutée aux objets, de nombreuses utilisations courantes en numérique sont devenues plus rapides, simplement en permettant aux bibliothèques déjà présentes d'arrêter de faire des copies complètes sur les opérations courantes. En raison du succès spectaculaire de la sémantique des mouvements, d'autres domaines de la langue, traditionnellement dominés par des idiomes tels que la copie et l'échange, sont vus sous un jour nouveau et standardisés. std :: array est un exemple d'une telle réduction de force; alors que, comme auparavant, la plupart des rédacteurs standard auraient dit "utilisez des vecteurs, ils font tout ce que vous voulez et qui se soucient s'ils sont lents", maintenant l'appel est pour des conteneurs plus spécialisés et spécifiques, tels que le tableau statique std :: array.

Alors pourquoi échanger?

Si vous regardez boost :: swap vous comprendrez pourquoi nous avons besoin du nouvel opérateur de swap: la recherche dépendante des arguments est difficile à encapsuler et à utiliser correctement, et entraîne une explosion des fonctions nécessaires, où l'idée de base de donner simplement une fonction de membre d'échange est assez simple. Avoir un opérateur qui peut le faire et fournir un opérateur de swap par défaut qui peut ensuite être utilisé pour un Copy And Swap par défaut est une énorme amélioration des performances.

Pourquoi? Parce que std :: swap est défini en termes de MoveConstructible et MoveAssignable en C++ 11 (anciennement construction de copie et affectation de copie, en C++ 98); cela nécessite trois mouvements et un temporaire (beaucoup plus rapide que les copies complètes nécessaires en C++ 98). C'est générique, et assez rapide, mais pas aussi rapide qu'un échange personnalisé (qui peut être 2-3x plus rapide, en supprimant le temporaire et un mouvement dans de nombreux cas). std :: swap dépend également du type étant nothrow-move-constructible et nothrow-move-assignable; il est concevable de penser à une classe qui ne l'est pas, mais qui pourrait fournir des garanties d'exception sur un swap personnalisé, évitant ainsi un comportement indéfini.

ADL et std :: swap peuvent interagir très bien, mais la syntaxe est quelque peu étrange; vous ajoutez

using std::swap;

à votre fonction appelant swap, et fournissez une fonction d'ami gratuite comme spécialisation de swap. Remplacer cet étrange cas de coin ADL implicite par un opérateur explicite serait plus facile pour les yeux, mais comme indiqué, il semble être mort à l'arrivée.

Exchange est une bête très similaire

En utilisant std :: move en échange, une copie complète n'est plus nécessaire. En utilisant une référence universelle pour new_val, la nouvelle valeur peut être parfaitement transmise ou déplacée directement dans son nouvel emplacement. En théorie, l'échange peut fonctionner avec absolument zéro copie, juste deux mouvements.

En résumé

Pourquoi est-ce nécessaire? Parce qu'il est rapide et n'impose aucun coût aux utilisateurs finaux, et étend C++ comme une alternative utile à Fortran dans le calcul scientifique.

6
Alice