fonction de membre d'échange d'ami public

169

Dans la belle réponse à l' idiome copier-et-échanger, il y a un morceau de code dont j'ai besoin d'un peu d'aide:

class dumb_array
{
public:
    // ...
    friend void swap(dumb_array& first, dumb_array& second) // nothrow
    {
        using std::swap; 
        swap(first.mSize, second.mSize); 
        swap(first.mArray, second.mArray);
    }
    // ...
};

et il ajoute une note

Il y a d'autres prétentions que nous devrions spécialiser std :: swap pour notre type, fournir un swap en classe avec un swap de fonction libre, etc. Mais tout cela est inutile: toute utilisation correcte de swap se fera par un appel non qualifié , et notre fonction sera trouvée via ADL. Une fonction fera l'affaire.

Avec friendje suis un peu "hostile", je dois l'admettre. Donc, mes principales questions sont:

  • ressemble à une fonction libre , mais c'est à l'intérieur du corps de la classe?
  • pourquoi n'est-ce pas swapstatique ? Il n'utilise évidemment aucune variable membre.
  • "Toute utilisation appropriée du swap permettra de découvrir le swap via ADL" ? ADL recherchera les espaces de noms, non? Mais regarde-t-il aussi à l'intérieur des classes? Ou est-ce ici où friendintervient?

Questions secondaires:

  • Avec C ++ 11, dois-je marquer mon swaps avec noexcept?
  • Avec C ++ 11 et son range-for , dois-je placer friend iter begin()et de friend iter end()la même manière à l'intérieur de la classe? Je pense que ce friendn'est pas nécessaire ici, non?
Towi
la source
Compte tenu de la question secondaire sur la base de la plage pour: il est préférable d'écrire des fonctions membres et de laisser l'accès à la plage sur begin () et end () dans l'espace de noms std (§24.6.5), basé sur la plage pour les utilise en interne à partir de global ou espace de noms std (voir §6.5.4). Cependant, il y a un inconvénient que ces fonctions font partie de l'en-tête <iterator>, si vous ne l'incluez pas, vous voudrez peut-être les écrire vous-même.
Vitus
2
pourquoi n'est-ce pas statique - parce qu'une friendfonction n'est pas du tout une fonction membre.
aschepler

Réponses:

175

Il existe plusieurs façons d'écrire swap, certaines meilleures que d'autres. Au fil du temps, cependant, il a été constaté qu'une seule définition fonctionnait le mieux. Voyons comment nous pourrions penser l'écriture d'une swapfonction.


Nous voyons d'abord que les conteneurs comme std::vector<>ont une fonction membre à argument unique swap, telle que:

struct vector
{
    void swap(vector&) { /* swap members */ }
};

Naturellement, alors, notre classe devrait aussi, non? Eh bien pas vraiment. La bibliothèque standard contient toutes sortes de choses inutiles , et un membre en swapfait partie. Pourquoi? Continuons.


Ce que nous devons faire, c'est identifier ce qui est canonique et ce que notre classe doit faire pour travailler avec. Et la méthode canonique d'échange est avec std::swap. C'est pourquoi les fonctions membres ne sont pas utiles: elles ne sont pas la façon dont nous devrions échanger les choses, en général, et n'ont aucune incidence sur le comportement de std::swap.

Eh bien, pour faire du std::swaptravail, nous devrions fournir (et std::vector<>aurions dû fournir) une spécialisation std::swap, non?

namespace std
{
    template <> // important! specialization in std is OK, overloading is UB
    void swap(myclass&, myclass&)
    {
        // swap
    }
}

Eh bien, cela fonctionnerait certainement dans ce cas, mais cela pose un problème flagrant: les spécialisations de fonctions ne peuvent pas être partielles. Autrement dit, nous ne pouvons pas spécialiser les classes de modèles avec ceci, seulement des instanciations particulières:

namespace std
{
    template <typename T>
    void swap<T>(myclass<T>&, myclass<T>&) // error! no partial specialization
    {
        // swap
    }
}

Cette méthode fonctionne parfois, mais pas tout le temps. Il doit y avoir un meilleur moyen.


Il y a! Nous pouvons utiliser une friendfonction et la trouver via ADL :

namespace xyz
{
    struct myclass
    {
        friend void swap(myclass&, myclass&);
    };
}

Quand nous voulons échanger quelque chose, nous associons std::swap puis faisons un appel non qualifié:

using std::swap; // allow use of std::swap...
swap(x, y); // ...but select overloads, first

// that is, if swap(x, y) finds a better match, via ADL, it
// will use that instead; otherwise it falls back to std::swap

Qu'est-ce qu'une friendfonction? Il y a confusion dans ce domaine.

Avant que C ++ ne soit normalisé, les friendfonctions faisaient quelque chose appelé «injection de nom d'ami», où le code se comportait comme si la fonction avait été écrite dans l'espace de noms environnant. Par exemple, il s'agissait de pré-normes équivalentes:

struct foo
{
    friend void bar()
    {
        // baz
    }
};

// turned into, pre-standard:    

struct foo
{
    friend void bar();
};

void bar()
{
    // baz
}

Cependant, lorsque ADL a été inventé, cela a été supprimé. La friendfonction pourrait alors ne se trouve par ADL; si vous le vouliez en tant que fonction libre, il fallait le déclarer comme tel ( voir ceci , par exemple). Mais voilà! Il y avait un problème.

Si vous utilisez simplement std::swap(x, y), votre surcharge ne sera jamais trouvée, car vous avez explicitement dit "regarder dedans std, et nulle part ailleurs"! C'est pourquoi certains ont suggéré d'écrire deux fonctions: l'une comme fonction à trouver via ADL , et l'autre pour gérer des std::qualifications explicites .

Mais comme nous l'avons vu, cela ne peut pas fonctionner dans tous les cas et nous nous retrouvons avec un désordre horrible. Au lieu de cela, l'échange idiomatique est allé dans l'autre voie: au lieu d'en faire le travail des classes à fournir std::swap, c'est le travail des échangeurs de s'assurer qu'ils n'utilisent pas qualifié swap, comme ci-dessus. Et cela a tendance à bien fonctionner, tant que les gens le savent. Mais c'est là que réside le problème: il n'est pas intuitif de devoir utiliser un appel non qualifié!

Pour rendre cela plus facile, certaines bibliothèques comme Boost ont fourni la fonction boost::swap, qui ne fait qu'un appel non qualifié à swap, avec std::swapcomme espace de noms associé. Cela aide à rendre les choses succinctes à nouveau, mais c'est toujours une déception.

Notez qu'il n'y a pas de changement dans le comportement de C ++ 11 std::swap, ce que moi et d'autres pensions à tort que ce serait le cas. Si cela vous a mordu, lisez ici .


En bref: la fonction membre n'est que du bruit, la spécialisation est moche et incomplète, mais la friendfonction est complète et fonctionne. Et quand vous échangez, utilisez boost::swapou non qualifié swapavec std::swapassocié.


† De manière informelle, un nom est associé s'il sera pris en compte lors d'un appel de fonction. Pour plus de détails, lisez le §3.4.2. Dans ce cas, std::swapn'est normalement pas pris en compte; mais on peut l' associer (l'ajouter à l'ensemble des surcharges considérées par les non qualifiés swap), permettant de la retrouver.

GManNickG
la source
10
Je ne suis pas d'accord pour dire que la fonction de membre n'est que du bruit. Une fonction membre permet par exemple std::vector<std::string>().swap(someVecWithData);, ce qui n'est pas possible avec une swapfonction libre car les deux arguments sont passés par référence non-const.
ildjarn
3
@ildjarn: Vous pouvez le faire sur deux lignes. Avoir la fonction membre viole le principe DRY.
GManNickG
4
@GMan: Le principe DRY ne s'applique pas si l'un est implémenté en terme de l'autre. Sinon , personne ne préconiserait une classe avec des implémentations de operator=, operator+et operator+=, mais clairement les opérateurs sur les classes concernées ne sont acceptées / devraient exister pour la symétrie. Il en va de même pour membre swap+ espace de noms swapà mon avis.
ildjarn
3
@GMan Je pense qu'il considère trop de fonctions. Peu connu, mais même un function<void(A*)> f; if(!f) { }peut échouer simplement parce qu'il Adéclare un operator!qui accepte faussi bien que fle sien operator!(peu probable, mais cela peut arriver). Si l function<>'auteur de l' auteur pensait "ohh j'ai un 'operator bool', pourquoi devrais-je implémenter 'operator!'? Cela violerait DRY!", Ce serait fatal. Vous avez juste besoin d'avoir un operator!implémenté pour A, et d' Aavoir un constructeur pour a function<...>, et les choses vont casser, car les deux candidats nécessiteront des conversions définies par l'utilisateur.
Johannes Schaub - litb
1
Voyons comment nous pourrions penser à l'écriture d'une fonction d'échange [membre]. Naturellement, alors, notre classe devrait aussi, non? Eh bien pas vraiment. La bibliothèque standard contient toutes sortes de choses inutiles , et un échange de membre en fait partie. Le GotW lié préconise la fonction d'échange de membres.
Xeverous
7

Ce code est équivalent (dans presque tous les sens) à:

class dumb_array
{
public:
    // ...
    friend void swap(dumb_array& first, dumb_array& second);
    // ...
};

inline void swap(dumb_array& first, dumb_array& second) // nothrow
{
    using std::swap; 
    swap(first.mSize, second.mSize); 
    swap(first.mArray, second.mArray);
}

Une fonction ami définie à l'intérieur d'une classe est:

  • placé dans l'espace de noms englobant
  • automatiquement inline
  • capable de faire référence à des membres statiques de la classe sans autre qualification

Les règles exactes sont dans la section [class.friend](je cite les paragraphes 6 et 7 du projet C ++ 0x):

Une fonction peut être définie dans une déclaration d'ami d'une classe si et seulement si la classe est une classe non locale (9.8), le nom de la fonction n'est pas qualifié et la fonction a une portée d'espace de noms.

Une telle fonction est implicitement en ligne. Une fonction friend définie dans une classe est dans la portée (lexicale) de la classe dans laquelle elle est définie. Une fonction ami définie en dehors de la classe ne l'est pas.

Ben Voigt
la source
2
En fait, les fonctions friend ne sont pas placées dans l'espace de noms englobant, en C ++ standard. L'ancien comportement s'appelait «injection de nom d'ami», mais a été remplacé par ADL, remplacé dans la première norme. Voir le haut de ceci . (Le comportement est assez similaire, cependant.)
GManNickG
1
Pas vraiment équivalent. Le code de la question fait en sorte que ce swapn'est visible que par ADL. C'est un membre de l'espace de noms englobant, mais son nom n'est pas visible pour les autres formulaires de recherche de nom. EDIT: Je vois que @GMan était à nouveau plus rapide :) @Ben il en a toujours été ainsi dans l'ISO C ++ :)
Johannes Schaub - litb
2
@Ben: Non, l'injection d'amis n'a jamais existé dans un standard, mais elle était largement utilisée auparavant, c'est pourquoi l'idée (et le support du compilateur) avaient tendance à continuer, mais techniquement, ce n'est pas là. friendles fonctions ne sont trouvées que par ADL, et si elles doivent simplement être des fonctions libres avec friendaccès, elles doivent être déclarées à la fois comme frienddans la classe et comme une déclaration de fonction libre normale en dehors de la classe. Vous pouvez voir cette nécessité dans cette réponse , par exemple.
GManNickG
2
@towi: Puisque la fonction ami est à portée d'espace de noms, les réponses à vos trois questions devraient devenir claires: (1) C'est une fonction gratuite, en plus d'avoir un accès ami aux membres privés et protégés de la classe. (2) Ce n'est pas du tout un membre, ni instance ni statique. (3) ADL ne recherche pas dans les classes mais c'est correct car la fonction friend a une portée d'espace de noms.
Ben Voigt
1
@Ben. Dans la spécification, la fonction est un membre de l'espace de noms, et l'expression «la fonction a une portée d'espace de noms» peut être interprétée pour dire que la fonction est un membre d'espace de noms (cela dépend à peu près du contexte d'une telle instruction). Et il ajoute un nom à cet espace de noms qui n'est visible que par ADL (en fait, certaines parties de l'IIRC contredisent d'autres parties de la spécification pour savoir si un nom est ajouté ou non. Mais l'ajout d'un nom est nécessaire pour détecter les déclarations incompatibles ajoutées à cela namespace, donc en fait, un nom invisible est ajouté (voir la note en 3.3.1p4).
Johannes Schaub - litb le