web-dev-qa-db-fra.com

fonction de membre d'échange d'ami public

Dans la belle réponse à l'idiome copy-and-swap-idiom il y a un morceau de code dont j'ai besoin 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 revendications selon lesquelles nous devrions spécialiser std :: swap pour notre type, fournir un swap en classe avec un swap de fonction libre, etc. , et notre fonction sera trouvée via ADL. Une fonction fera l'affaire.

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

  • ressemble à une fonction libre , mais est-ce à l'intérieur du corps de la classe?
  • pourquoi n'est-ce pas swap statique ? Il n'utilise évidemment aucune variable membre.
  • "Toute utilisation appropriée du swap permettra de découvrir le swap via ADL" ? ADL va rechercher les espaces de noms, non? Mais regarde-t-il également à l'intérieur des classes? Ou est-ce ici que friend entre en jeu?

Questions secondaires:

  • Avec C++ 11, dois-je marquer mes swaps avec noexcept?
  • Avec C++ 11 et sa plage pour , dois-je placer friend iter begin() et friend iter end() de la même manière à l'intérieur de la classe? Je pense que le friend n'est pas nécessaire ici, non?
149
towi

Il existe plusieurs façons d'écrire swap, certaines mieux que d'autres. Au fil du temps, cependant, il s'est avéré qu'une seule définition fonctionnait mieux. Voyons comment nous pourrions penser à écrire une fonction swap.


Nous voyons d'abord que les conteneurs comme std::vector<> Ont une fonction membre à un seul argument 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 a toutes sortes de choses inutiles , et un membre swap en fait partie. Pourquoi? Continuons.


Ce que nous devons faire est d'identifier ce qui est canonique et ce que notre classe a besoin de 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 comme nous devons échanger les choses, en général, et n'ont aucune incidence sur le comportement de std::swap.

Eh bien, pour que std::swap Fonctionne, nous devons fournir (et std::vector<> Aurait dû fournir) une spécialisation de 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 il a un problème flagrant: les spécialisations de fonction ne peuvent pas être partielles. Autrement dit, nous ne pouvons pas spécialiser les classes de modèles avec cela, 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 une partie du temps, mais pas tout le temps. Il doit y avoir un meilleur moyen.


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

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

Lorsque nous voulons échanger quelque chose, nous associons std::swap Puis passez un appel sans réserve:

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 fonction friend? Il y a de la confusion dans ce domaine.

Avant la normalisation de C++, les fonctions friend faisaient quelque chose appelé "injection de nom d'ami", où le code se comportait comme si si la fonction avait été écrit dans l'espace de noms environnant. Par exemple, ceux-ci étaient pré-standard équivalents:

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 fonction friend pourrait alors seulement être trouvée via ADL; si vous la vouliez comme fonction libre, elle devait être déclarée ainsi ( 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 "regardez dans std, et nulle part ailleurs "! C'est pourquoi certaines personnes ont suggéré d'écrire deux fonctions: l'une en tant que fonction à trouver via ADL, et l'autre pour gérer les qualifications explicites std::.

Mais comme nous l'avons vu, cela ne peut pas fonctionner dans tous les cas, et nous nous retrouvons avec un désordre laid. Au lieu de cela, l'échange idiomatique est allé dans l'autre sens: au lieu d'en faire le travail des classes pour fournir std::swap, C'est le travail des échangeurs de s'assurer qu'ils n'utilisent pas de swap qualifié, 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 fait juste un appel non qualifié à swap, avec std::swap Comme espace de noms associé. Cela aide à rendre les choses encore succinctes, mais c'est toujours une déception.

Notez qu'il n'y a aucun changement en C++ 11 au comportement de std::swap, Ce que moi et d'autres pensions à tort que ce serait le cas. Si vous avez été mordu par cela, lire ici .


En bref: la fonction membre est juste du bruit, la spécialisation est moche et incomplète, mais la fonction friend est complète et fonctionne. Et lorsque vous échangez, utilisez boost::swap Ou un swap non qualifié avec std::swap Associé.


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

158
GManNickG

Ce code est équivalent (dans presque dans 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 amie définie dans une classe est:

  • placé dans l'espace de noms englobant
  • automatiquement inline
  • pouvoir se référer à des membres statiques de la classe sans autre qualification

Les règles exactes se trouvent 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 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 amie 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.

7
Ben Voigt