Comment surcharger std :: swap ()

std::swap() est utilisé par de nombreux conteneurs std (tels que std::list et std::vector ) lors du sorting et même de 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, vous pouvez gagner en efficacité en surchargeant std::swap() avec une implémentation spécifique à un type personnalisé. Mais comment pouvez-vous l’implémenter pour qu’il soit utilisé par les conteneurs std?

Le bon moyen de surcharger le swap consiste à l’écrire dans le même espace de nommage que ce que vous échangez, afin de le retrouver via une recherche ADL (dépendante des arguments). 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); // ... } }; 

Attention Mozza314

Voici une simulation des effets d’un std::algorithm générique appelant std::swap , et demandant à l’utilisateur de fournir son swap dans son espace de noms std. Comme il s’agit d’une expérience, cette simulation utilise l’ namespace exp au lieu de l’ namespace std de namespace std .

 // simulate  #include  namespace exp { template  void swap(T& x, T& y) { printf("generic exp::swap\n"); T tmp = x; x = y; y = tmp; } template  void algorithm(T* begin, T* end) { if (end-begin >= 2) exp::swap(begin[0], begin[1]); } } // simulate user code which includes  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 cela 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), il donnera le même résultat que celui que je montre. Et dans ce cas, exactement ce que vous craignez se produira, se produit. Et mettre votre swap dans namespace std ( exp ) ne l’a pas empêché de se produire.

Dave et moi sums membres du comité et travaillons dans ce domaine depuis une décennie (et pas toujours en accord). Mais cette question est réglée depuis longtemps et nous sums tous les deux d’accord sur la manière dont cela a été réglé. Ne tenez pas compte de l’avis / réponse 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 c’est la solution moderne:

 // simulate  #include  namespace exp { template  void swap(T& x, T& y) { printf("generic exp::swap\n"); T tmp = x; x = y; y = tmp; } template  void algorithm(T* begin, T* end) { if (end-begin >= 2) swap(begin[0], begin[1]); } } // simulate user code which includes  struct A { }; void swap(A&, A&) { printf("swap(A, A)\n"); } // exercise simulation int main() { A a[2]; exp::algorithm(a, a+2); } 

Le résultat 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ù votre A est un modèle de classe:

 // simulate user code which includes  template  struct A { }; namespace exp { template  void swap(A&, A&) { printf("exp::swap(A, A)\n"); } } // exercise simulation int main() { A a[2]; exp::algorithm(a, a+2); } 

Maintenant, ça ne marche plus. 🙁

Vous pouvez donc mettre swap dans namespace std et le faire fonctionner. Mais vous devez vous rappeler de mettre swap dans l’espace de noms de A pour le cas où vous avez un template: A . Et comme les deux cas fonctionneront si vous placez le swap dans l’espace de noms de A, il est plus facile de se souvenir (et d’apprendre aux autres) à le faire d’une seule façon.

Vous n’êtes pas autorisé (par la norme C ++) à surcharger std :: swap, mais vous êtes spécifiquement autorisé à append des spécialisations de modèles 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 usages dans les conteneurs std (et ailleurs) choisiront votre spécialisation au lieu du général.

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 objects dérivés, il utilisera la version générique de std car l’échange de modèles est une correspondance exacte (et évite le problème de ne permuter que les parties “de base” de vos objects dérivés). ).

REMARQUE: je l’ai mis à jour pour supprimer les mauvais bits de ma dernière réponse. D’oh! (merci puetzk et j_random_hacker de l’avoir signalé)

Bien qu’il soit correct de ne pas append de choses à std :: namespace, l’ajout de spécialisations de modèles 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 est indéfini pour un programme C ++ d’append des déclarations ou des définitions à des espaces de noms std ou namespaces avec namespace std, sauf indication contraire. Un programme peut append des spécialisations de modèle pour tout modèle de bibliothèque standard à namespace 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 liaison externe défini par l’utilisateur et que la spécialisation de modèle réponde aux exigences de bibliothèque standard du 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 template <> bit ce serait une surcharge, qui n’est pas définie, plutôt qu’une spécialisation, ce qui est autorisé. L’approche suggérée par @ Wilka pour changer l’espace de nommage 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 elle n’est pas garantie et l’implémentation STL doit utiliser pleinement -qualifié std :: swap).

Il y a un thread sur comp.lang.c ++ modéré avec une longue discussion du sujet. La plupart d’entre elles concernent la spécialisation partielle (ce qui n’est actuellement pas un bon moyen de le faire).