Comment surcharger std :: swap ()

115

std::swap()est utilisé par de nombreux conteneurs std (tels que std::listet std::vector) pendant le tri et même l'affectation.

Mais l'implémentation std de swap()est très généralisée et plutôt inefficace pour les types personnalisés.

Ainsi, l'efficacité peut être gagnée en surchargeant std::swap()avec une implémentation spécifique de type personnalisé. Mais comment pouvez-vous l'implémenter pour qu'il soit utilisé par les conteneurs std?

Adam
la source
Informations connexes: en.cppreference.com/w/cpp/concept/Swappable
Pharap
La permutable la page a déménagé à en.cppreference.com/w/cpp/named_req/Swappable
Andrew Keeton

Réponses:

135

La bonne façon de surcharger le swap est de l'écrire dans le même espace de noms que celui que vous échangez, afin qu'il puisse être trouvé via la recherche dépendante de l'argument (ADL) . Une chose particulièrement facile à faire est:

class X
{
    // ...
    friend void swap(X& a, X& b)
    {
        using std::swap; // bring in swap for built-in types

        swap(a.base1, b.base1);
        swap(a.base2, b.base2);
        // ...
        swap(a.member1, b.member1);
        swap(a.member2, b.member2);
        // ...
    }
};
Dave Abrahams
la source
11
Dans C ++ 2003, il est au mieux sous-spécifié. La plupart des implémentations utilisent ADL pour trouver le swap, mais non, ce n'est pas obligatoire, vous ne pouvez donc pas compter dessus. Vous pouvez spécialiser std :: swap pour un type concret spécifique comme indiqué par l'OP; ne vous attendez pas à ce que cette spécialisation soit utilisée, par exemple pour les classes dérivées de ce type.
Dave Abrahams
15
Je serais surpris de constater que les implémentations n'utilisent toujours pas ADL pour trouver le bon swap. C'est un vieux problème au comité. Si votre implémentation n'utilise pas ADL pour trouver le swap, déposez un rapport de bogue.
Howard Hinnant
3
@Sascha: Tout d'abord, je définis la fonction au niveau de l'espace de noms car c'est le seul type de définition qui compte pour le code générique. Parce que int et. Al. ne pas / ne peut pas avoir de fonctions membres, std :: sort et. Al. doivent utiliser une fonction gratuite; ils établissent le protocole. Deuxièmement, je ne sais pas pourquoi vous vous opposez à avoir deux implémentations, mais la plupart des classes sont vouées à être triées de manière inefficace si vous ne pouvez pas accepter un échange de non-membres. Les règles de surcharge garantissent que si les deux déclarations sont vues, la plus spécifique (celle-ci) sera choisie lorsque le swap est appelé sans qualification.
Dave Abrahams
5
@ Mozza314: Cela dépend. Un std::sortqui utilise ADL pour permuter des éléments est C ++ 03 non conforme mais conforme C ++ 11. Aussi, pourquoi -1 une réponse basée sur le fait que les clients peuvent utiliser du code non idiomatique?
JoeG
4
@curiousguy: Si la lecture de la norme n'était qu'une simple question de lecture de la norme, vous auriez raison :-). Malheureusement, l'intention des auteurs est importante. Donc, si l'intention initiale était que l'ADL puisse ou devrait être utilisée, elle est sous-spécifiée. Sinon, c'est juste un changement de rupture pour C ++ 0x, c'est pourquoi j'ai écrit «au mieux» sous-spécifié.
Dave Abrahams
70

Attention Mozza314

Voici une simulation des effets d'un std::algorithmappel générique std::swap, et demandant à l'utilisateur de fournir son échange dans l'espace de noms std. Comme il s'agit d'une expérience, cette simulation utilise à la namespace expplace de namespace std.

// simulate <algorithm>

#include <cstdio>

namespace exp
{

    template <class T>
    void
    swap(T& x, T& y)
    {
        printf("generic exp::swap\n");
        T tmp = x;
        x = y;
        y = tmp;
    }

    template <class T>
    void algorithm(T* begin, T* end)
    {
        if (end-begin >= 2)
            exp::swap(begin[0], begin[1]);
    }

}

// simulate user code which includes <algorithm>

struct A
{
};

namespace exp
{
    void swap(A&, A&)
    {
        printf("exp::swap(A, A)\n");
    }

}

// exercise simulation

int main()
{
    A a[2];
    exp::algorithm(a, a+2);
}

Pour moi, ceci s'imprime:

generic exp::swap

Si votre compilateur imprime quelque chose de différent, il n'implémente pas correctement la "recherche en deux phases" pour les modèles.

Si votre compilateur est conforme (à l'un des C ++ 98/03/11), alors il donnera la même sortie que je montre. Et dans ce cas, exactement ce que vous craignez se produira. Et mettre votre swapdans namespace std( exp) ne l'a pas empêché de se produire.

Dave et moi sommes tous deux membres du comité et travaillons dans ce domaine de la norme depuis une décennie (et pas toujours en accord l'un avec l'autre). Mais cette question est réglée depuis longtemps et nous sommes tous les deux d'accord sur la façon dont elle a été réglée. Ne tenez pas compte de l'opinion / de la réponse d'expert de Dave dans ce domaine à vos risques et périls.

Ce problème est apparu après la publication de C ++ 98. À partir de 2001, Dave et moi avons commencé à travailler dans ce domaine . Et voici la solution moderne:

// simulate <algorithm>

#include <cstdio>

namespace exp
{

    template <class T>
    void
    swap(T& x, T& y)
    {
        printf("generic exp::swap\n");
        T tmp = x;
        x = y;
        y = tmp;
    }

    template <class T>
    void algorithm(T* begin, T* end)
    {
        if (end-begin >= 2)
            swap(begin[0], begin[1]);
    }

}

// simulate user code which includes <algorithm>

struct A
{
};

void swap(A&, A&)
{
    printf("swap(A, A)\n");
}

// exercise simulation

int main()
{
    A a[2];
    exp::algorithm(a, a+2);
}

La sortie est:

swap(A, A)

Mettre à jour

Une observation a été faite que:

namespace exp
{    
    template <>
    void swap(A&, A&)
    {
        printf("exp::swap(A, A)\n");
    }

}

travaux! Alors pourquoi ne pas l'utiliser?

Considérez le cas où vous Aêtes un modèle de classe:

// simulate user code which includes <algorithm>

template <class T>
struct A
{
};

namespace exp
{

    template <class T>
    void swap(A<T>&, A<T>&)
    {
        printf("exp::swap(A, A)\n");
    }

}

// exercise simulation

int main()
{
    A<int> a[2];
    exp::algorithm(a, a+2);
}

Maintenant, cela ne fonctionne plus. :-(

Vous pouvez donc mettre swapdans l'espace de noms std et le faire fonctionner. Mais vous devez vous rappeler de mettre swapdans Al'espace de nom » pour le cas où vous avez un modèle: A<T>. Et puisque les deux cas fonctionneront si vous mettez swapdans Al'espace de noms », il est tout simplement plus facile à retenir (et à d' autres enseignent) à faire juste que d' une façon.

Howard Hinnant
la source
4
Merci beaucoup pour la réponse détaillée. Je suis clairement moins au courant à ce sujet et je me demandais en fait comment la surcharge et la spécialisation pourraient produire des comportements différents. Cependant, je ne suggère pas de surcharge mais de spécialisation. Quand je mets template <>votre premier exemple, j'obtiens la sortie exp::swap(A, A)de gcc. Alors, pourquoi ne pas préférer la spécialisation?
voltrevo
1
Hou la la! C'est vraiment éclairant. Vous m'avez définitivement convaincu. Je pense que je vais légèrement modifier votre suggestion et utiliser la syntaxe d'ami en classe de Dave Abrahams (hé je peux aussi l'utiliser pour l'opérateur <<! :-)), à moins que vous n'ayez une raison d'éviter cela également (autre que la compilation séparément). Aussi, à la lumière de cela, pensez-vous qu'il y using std::swapait une exception à la règle "ne jamais mettre en utilisant les instructions dans les fichiers d'en-tête"? En fait, pourquoi ne pas mettre à l' using std::swapintérieur <algorithm>? Je suppose que cela pourrait briser une infime minorité du code des gens. Peut-être abandonner le support et éventuellement le mettre en place?
voltrevo
3
la syntaxe de l'ami en classe devrait convenir. J'essaierais de limiter la using std::swapportée de la fonction dans vos en-têtes. Oui, swapc'est presque un mot-clé. Mais non, ce n'est pas tout à fait un mot-clé. Il vaut donc mieux ne pas l'exporter vers tous les espaces de noms tant que vous n'êtes pas vraiment obligé de le faire. swapest un peu comme operator==. La plus grande différence est que personne ne pense même à appeler operator==avec une syntaxe d'espace de noms qualifié (ce serait tout simplement trop moche).
Howard Hinnant
15
@NielKirk: Ce que vous voyez comme une complication est tout simplement trop de mauvaises réponses. Il n'y a rien de compliqué dans la réponse correcte de Dave Abrahams: "La bonne façon de surcharger l'échange est de l'écrire dans le même espace de noms que celui que vous échangez, afin qu'il puisse être trouvé via la recherche dépendante des arguments (ADL)."
Howard Hinnant
2
@codeshot: Désolé. Herb essaie de faire passer ce message depuis 1998: gotw.ca/publications/mill02.htm Il ne mentionne pas l'échange dans cet article. Mais ce n'est qu'une autre application du principe d'interface de Herb.
Howard Hinnant
53

Vous n'êtes pas autorisé (par le standard C ++) à surcharger std :: swap, mais vous êtes spécifiquement autorisé à ajouter des spécialisations de modèle pour vos propres types à l'espace de noms std. Par exemple

namespace std
{
    template<>
    void swap(my_type& lhs, my_type& rhs)
    {
       // ... blah
    }
}

alors les utilisations dans les conteneurs std (et partout ailleurs) choisiront votre spécialisation au lieu de la généraliste.

Notez également que fournir une implémentation de classe de base de swap n'est pas suffisant pour vos types dérivés. Par exemple, si vous avez

class Base
{
    // ... stuff ...
}
class Derived : public Base
{
    // ... stuff ...
}

namespace std
{
    template<>
    void swap(Base& lha, Base& rhs)
    {
       // ...
    }
}

cela fonctionnera pour les classes de base, mais si vous essayez d'échanger deux objets dérivés, il utilisera la version générique de std car le swap basé sur un modèle est une correspondance exacte (et cela évite le problème de ne permuter que les parties `` de base '' de vos objets dérivés ).

REMARQUE: j'ai mis à jour ceci pour supprimer les mauvais bits de ma dernière réponse. Oh! (merci puetzk et j_random_hacker pour l'avoir signalé)

Wilka
la source
1
Surtout un bon conseil, mais je dois -1 à cause de la distinction subtile notée par puetzk entre la spécialisation d'un modèle dans l'espace de noms std (ce qui est autorisé par le standard C ++) et la surcharge (ce qui ne l'est pas).
j_random_hacker
11
Évalué parce que la manière correcte de personnaliser le swap est de le faire dans votre propre espace de noms (comme le souligne Dave Abrahams dans une autre réponse).
Howard Hinnant
2
Mes raisons de voter sont les mêmes que celles de Howard
Dave Abrahams
13
@HowardHinnant, Dave Abrahams: Je ne suis pas d'accord. Sur quelle base prétendez-vous que votre alternative est la «bonne» voie? Comme puetzk l'a cité dans la norme, cela est spécifiquement autorisé. Bien que je sois nouveau dans ce problème, je n'aime vraiment pas la méthode que vous préconisez car si je définis Foo et que je change de cette façon, quelqu'un d'autre qui utilise mon code utilisera probablement std :: swap (a, b) plutôt que swap ( a, b) sur Foo, qui utilise silencieusement la version par défaut inefficace.
voltrevo
5
@ Mozza314: L'espace et les contraintes de formatage de la zone de commentaire ne m'ont pas permis de vous répondre pleinement. Veuillez voir la réponse que j'ai ajoutée intitulée "Attention Mozza314".
Howard Hinnant
29

Bien qu'il soit correct de ne pas généralement ajouter de choses à l'espace de noms std ::, l'ajout de spécialisations de modèle pour les types définis par l'utilisateur est spécifiquement autorisé. La surcharge des fonctions ne l'est pas. C'est une différence subtile :-)

17.4.3.1/1 Il n'est pas défini pour un programme C ++ d'ajouter des déclarations ou des définitions à l'espace de noms std ou aux espaces de noms avec l'espace de noms std, sauf indication contraire. Un programme peut ajouter des spécialisations de modèle pour tout modèle de bibliothèque standard à l'espace de noms std. Une telle spécialisation (complète ou partielle) d'une bibliothèque standard entraîne un comportement indéfini à moins que la déclaration ne dépende d'un nom de lien externe défini par l'utilisateur et à moins que la spécialisation de modèle ne réponde aux exigences de bibliothèque standard pour le modèle d'origine.

Une spécialisation de std :: swap ressemblerait à ceci:

namespace std
{
    template<>
    void swap(myspace::mytype& a, myspace::mytype& b) { ... }
}

Sans le bit template <>, ce serait une surcharge, qui n'est pas définie, plutôt qu'une spécialisation, qui est autorisée. @ L'approche suggérée par Wilka de changer l'espace de noms par défaut peut fonctionner avec le code utilisateur (en raison de la recherche Koenig préférant la version sans espace de noms) mais ce n'est pas garanti, et en fait n'est pas vraiment censée le faire (l'implémentation STL devrait utiliser pleinement -std :: swap qualifié).

Il existe un fil de discussion sur comp.lang.c ++. Animé par une longue discussion sur le sujet. Cependant, la plupart d'entre eux concernent une spécialisation partielle (ce qu'il n'y a actuellement aucun bon moyen de faire).

puetzk
la source
7
Une des raisons pour lesquelles il est faux d'utiliser la spécialisation de modèle de fonction pour cela (ou quoi que ce soit): cela interagit de manière négative avec les surcharges, dont il y en a beaucoup pour le swap. Par exemple, si vous spécialisez le standard std :: swap pour std :: vector <mytype> &, votre spécialisation ne sera pas choisie par rapport au swap spécifique au vecteur du standard, car les spécialisations ne sont pas prises en compte lors de la résolution de surcharge.
Dave Abrahams
4
C'est également ce que recommande Meyers dans Effective C ++ 3ed (Item 25, pp 106-112).
jww
2
@DaveAbrahams: Si vous vous spécialisez (sans arguments de modèle explicites), ordre partiel entraînera à être une spécialisation de lavector version et elle sera utilisée .
Davis Herring
1
@DavisHerring en fait, non, quand vous faites cet ordre partiel ne joue aucun rôle. Le problème n'est pas que vous ne pouvez pas l'appeler du tout; c'est ce qui se passe en présence de surcharges de swap apparemment moins spécifiques: wandbox.org/permlink/nck8BkG0WPlRtavV
Dave Abrahams
2
@DaveAbrahams: L'ordre partiel consiste à sélectionner le modèle de fonction à spécialiser lorsque la spécialisation explicite correspond à plus d'un. La ::swapsurcharge que vous avez ajoutée est plus spécialisée que la std::swapsurcharge vector, elle capture donc l'appel et aucune spécialisation de ce dernier n'est pertinente. Je ne sais pas en quoi c'est un problème pratique (mais je ne prétends pas non plus que c'est une bonne idée!).
Davis Herring