IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)

Apprendre à implémenter un framework personnalisable

Dans ce tutoriel, je veux décrire un problème que mes collègues ont rencontré à plusieurs reprises récemment et montrer comment il peut être résolu en C++. Voici le but : nous allons offrir une fonction (ou un ensemble de fonctions surchargées) qui fait « le bon travail » pour « le plus de types que possible ».

Commentez Donner une note à l´article (5)

Article lu   fois.

Les deux auteur et traducteur

Site personnel

Traducteur : Profil Pro

Liens sociaux

Viadeo Twitter Facebook Share on Google+   

I. Introduction

À titre d'exemple d'un tel « travail », considérons std::hash: ce que nous voulons éviter est la situation où vous voulez utiliser un certain type X comme une clé dans la table de hash standard, mais cette action vous est refusée, car std::hash ne fonctionne pas avec « X ». Afin de minimiser la déception, la bibliothèque standard s'assure que std::hash fonctionne avec tous les types de base ainsi que ceux de la bibliothèque standard. Pour tous les autres types, que la bibliothèque standard ne peut pas connaître par avance, il offre un moyen de personnaliser std::hash afin que ces types puissent fonctionner avec une table de hash.

Pour un autre exemple populaire, considérons la bibliothèque Boost.Serialization. Son objectif est que presque tout type doit être sérialisable avec la même interface : la bibliothèque sait comment sérialiser les types populaires de base, de std et de boost, et elle offre un moyen d'apprendre à sérialiser d'autres types.

Nous allons voir un certain nombre de techniques pour implémenter un framework personnalisable. Nous utiliserons les informations du post « Overload resolution ».

II. La tâche

Pour l'exemple, j'ai choisi une tâche assez simple, mais qui devrait servir à illustrer les problèmes pratiques que rencontrent les développeurs. Nous voulons être capables de connaître la taille qu'un objet donné occupe en mémoire : à la fois sur la pile et sur le tas. Laissez-moi vous donner un exemple ; si nous illustrons un std::vector comme suit :

Image non disponible

Nous pouvons voir une partie bleue représentant le « handle » : ce sont les trois pointeurs qui nous permettent d'avoir accès au reste des données ; la taille de cette partie peut être mesurée avec l'opérateur sizeof. La couleur verte représente la mémoire de tas allouée (oublions qu'il y a différents allocateurs pour le moment). Cela ne peut être mesuré avec sizeof et doit être calculé manuellement pour chaque type.

Maintenant, nous voulons fournir un framework capable de calculer l'utilisation de la mémoire pour les types de base et plusieurs types populaires, et d'offrir un moyen de calculer la mémoire nécessaire pour l'utilisation de nouveaux types.

Conceptuellement, le plan est le suivant. Nous allons avoir une fonction qui sera appelée mem_usage. Son utilisation sur un type donné X doit avoir les effets suivants :

  1. Pour les types scalaires, elle utilise simplement l'opérateur sizeof. En fait, cette stratégie peut être généralisée à tous les types trivialement copiables ;
  2. Nous fournissons une définition personnalisée pour un certain nombre de types communs que nous connaissons à l'avance (par exemple : std::vector, boost::optional) ;
  3. Pour les autres types, par défaut, une erreur de compilation devrait être émise ;
  4. Nous offrons un moyen pour les utilisateurs de personnaliser notre framework pour leurs propres types.

III. fonction membre requise - une non-solution

La première chose qui vient généralement à l'esprit dans ce cas, une sorte d'habitude, est de penser : nous allons exiger de chaque type qui participe à notre framework, qu'il offre une fonction membre mem_usage. Et nous l'utilisons tout simplement.

Mais cela ne fonctionnera pas. Alors que vous pouvez forcer vos collègues de l'équipe à ajouter la fonction membre mem_usage à toutes leurs classes, vous ne pouvez pas forcer les types de base scalaires intégrés à avoir un membre ; et vous ne pouvez pas forcer les types de std à avoir un membre de votre goût. En fait, cette exigence est irréaliste pour toute bibliothèque externe avec laquelle vous pourriez être amenés à travailler.

Une idée, peut-être même plus hardcore, est d'exiger que si vous voulez qu'un certain type X soit compatible avec notre framework, il doit dériver d'une classe d'interface polymorphe MemUsageAble.

Non seulement ça ne résout pas le problème (d'avoir un framework travaillant avec tout type), mais ça exige également que l'on augmente inutilement la taille occupée par les objets (ils doivent alors stocker un pointeur vers une vtable), et dans le cas de notre tâche cela affecte la valeur mesurée. Imaginons également que nous ayons besoin d'utiliser deux frameworks : l'un force les types à hériter d'une interface polymorphe, l'autre exige que les types héritent d'une autre. Cela devient insupportable.

Par conséquent, plutôt que d'attendre une fonction membre définie pour tous les types, nous ferions mieux de définir des fonctions en dehors du type : cela fonctionne de manière uniforme pour les types de base, les types d'une bibliothèque externe, et vos propres types.

IV. Surchages de fonction

D'après les conclusions du post précédent, nous savons déjà que nous ne pouvons pas utiliser les spécialisations des fonctions templates. Ainsi, pour notre première tentative, nous allons utiliser la surcharge de fonction (template) et faire confiance à l'ADL (Argument Dependant Lookup, la possibilité de rechercher une surcharge de fonction dans un namespace d'un de ses arguments).

Nous pouvons mettre en œuvre les spécifications 1 et 3 ci-dessus avec une fonction template. Afin de vérifier si un type donné est trivialement copiable ou non, nous pouvons utiliser le type trait std::is_trivially_copyable. Cependant, comme j'ai découvert que ce trait n'est pas disponible dans GCC jusqu'à la version 5.0, j'ai décidé d'en utiliser un autre : std::is_trivial, de sorte que les exemples fonctionnent sur plus de compilateurs.

Nous allons utiliser l'astuce de enable_if pour supprimer conditionnellement notre fonction de l'ensemble de surcharge :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
namespace framework
{
  template <typename T>
    typename std::enable_if<std::is_trivial<T>::value,
                            size_t>::type
    mem_usage1(const T& v) { return sizeof v; }
}

Le type trait std::is_trivial est uniquement disponible depuis C++11, mais sinon toutes les choses dont nous allons parler ici s'appliquent à C ++ 03. (et vous pouvez utiliser les bibliothèques Boost pour émuler certaines fonctionnalités manquantes (si vous utilisez Boost.TypeTraits ou Boost.StaticAssert)) Dans les exemples restants, je vais utiliser un alias de template C++14 std :: enable_if_t : ça rendra les exemples plus courts, mais le principe reste réalisable en C++03, avec une syntaxe un peu plus longue.

Maintenant, comment peut-on calculer l'utilisation mémoire de std::vector ? En supposant que l'allocateur est celui par défaut, c'est de la taille du handle + l'utilisation mémoire de chaque élément du vecteur (récursivement) + la capacité restante. Mais avant de passer à la mise en œuvre, nous devrons faire face à une question d'ordre technique : dans quel namespace doit-on définir la fonction ?

Il est raisonnable de supposer que notre framework viendra aussi avec un certain nombre d'«algorithmes» : des templates qui font usage de mem_usage1, par exemple :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
namespace framework
{
  namespace algo
  {
    template <typename T>
    size_t score(const T& v)
    {
      // on fait autres choses
      return mem_usage1(v);
    }
  }
}

Dans ce tutoriel, nous avons conclu que, pour que la surcharge soit indépendante de l'ordre d'inclusion des fichiers d'en-tête, nous devons déclarer notre surcharge dans le namespace contenant le type que nous surchargeons. Mais cela signifierait déclarer une surcharge de mem_usage1 dans le namespace std. Ce qui, à son tour, déclenche un comportement non défini. D'après la norme ([namespace.std] / 1) :

Le comportement d'un programme C ++ est indéfini s'il ajoute des déclarations ou des définitions au namespace std ou à un sous-namespace dans le namespace std, sauf indication contraire.

Heureusement, parce que std fait presque partie du langage, et chaque partie du programme le connaît ainsi que son contenu, nous pouvons définir notre mem_usage1 à l'intérieur du namespace framework, juste en dessous de la surcharge pour le cas général, avant tout autre fonction qui peut avoir besoin de l'utiliser :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
namespace framework
{
  // cas général:
  template <typename T>
    std::enable_if_t<std::is_trivial<T>::value, size_t>
    mem_usage1(const T& v) { return sizeof v; }
 
  // surcharge pour std::vector:
  template <typename T>
    size_t mem_usage1(const std::vector<T>& v)
    { 
      size_t ans = sizeof(v);
      for (const T& e : v) ans += mem_usage1(e);
      ans += (v.capacity() - v.size()) * sizeof(T);
      return ans;
    }
}

Nous pouvons nous en sortir ainsi, car std est très spéciale : toute autre bibliothèque dans le monde connaît std et peut l'inclure.

Cependant, il y a un prix que nous devons payer en échange. Maintenant, notre framework inclut inconditionnellement l'en-tête <vector>. Même l'utilisateur qui n'a jamais besoin d'utiliser des vecteurs inclut désormais indirectement cet en-tête standard.

Mais voici le premier problème. Supposons que nous voulons aussi fournir une surcharge pour std::pair. Nous pourrions les inclure dans l'ordre suivant :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
namespace framework
{
  // surcharge générale :
  template <typename T>
    std::enable_if_t<std::is_trivial<T>::value, size_t>
    mem_usage1(const T& v) { return sizeof v; }
 
  // surcharge pour std::vector :
  template <typename T>
    size_t mem_usage1(const std::vector<T>& v)
    { 
      size_t ans = sizeof(v);
      for (const T& e : v) ans += mem_usage1(e);
      ans += (v.capacity() - v.size()) * sizeof(T);
      return ans;
    }
 
  // surcharge pour std::pair :
  template <typename T, typename U>
    size_t mem_usage1(const std::pair<T, U>& v)
    { 
      return mem_usage1(v.first) + mem_usage1(v.second);
    }
}

Mais si nous voulons utiliser ces définitions avec le type std::vector<std::pair<int, int>>:

 
Sélectionnez
1.
2.
3.
4.
5.
int main()
{
  std::vector<std::pair<int, int>> vp;
  framework::mem_usage1(vp);
}

Nous obtenons une erreur de compilation. C'est en raison des règles de recherche de noms dans les templates :

  1. Dans le cas de surcharges définies dans les espaces de noms des types sur lesquels elles opèrent : nous pouvons les voir tous ;
  2. Dans le cas des surcharges définies dans l'espace de noms du template en question : on ne voit que les surcharges déclarées avant notre template.

Dans notre cas, nous commençons par sélectionner et compiler la surcharge pour std::vector, et cela fonctionne ; mais à l'intérieur de celle-ci, nous voulons trouver une surcharge pour std::pair, mais nous sommes dans le namespace framework, de sorte que nous ne voyons que les déclarations précédentes, et la surcharge pour std::pair est uniquement défini plus tard.

Si nous avions inversé les déclarations de surcharge, ça corrigerait notre problème dans ce cas, mais nous aurions alors un problème semblable pour le type std::pair<std::vector<int>, std::vector<int>>.

La manière de résoudre ce problème est d'utiliser des déclarations anticipées :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
namespace framework
{
  //  surcharge générale
  template <typename T>
    std::enable_if_t<std::is_trivial<T>::value, size_t>
    mem_usage1(const T& v) { return sizeof v; }
 
  //   déclaration anticipée de la surcharge pour std :: pair:
  template <typename T, typename U>
    size_t mem_usage1(const std::pair<T, U>& v);
 
  //  surcharge pour std::vector :
  template <typename T>
    size_t mem_usage1(const std::vector<T>& v)
    { 
      size_t ans = sizeof(v);
      for (const T& e : v) ans += mem_usage1(e);
      ans += (v.capacity() - v.size()) * sizeof(T);
      return ans;
    }
 
  //  surcharge pour std::pair :
  template <typename T, typename U>
    size_t mem_usage1(const std::pair<T, U>& v)
    { 
      return mem_usage1(v.first) + mem_usage1(v.second);
    }
}

Maintenant, supposons que nous voulons offrir une surcharge pour le type boost::optional. Cette tâche est un peu plus facile, parce que le namespace boost n'est pas spécial, et on a le droit d'y ajouter des déclarations :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
namespace boost
{
  template <typename T>
    size_t mem_usage1(const optional<T>& v)
    {
      using framework::mem_usage1;
       
      size_t ans = sizeof(v);
      if (v) ans += mem_usage1(*v) - sizeof(*v);
      return ans;
    }
}

La mémoire occupée par boost::optional est son sizeof (l'indicateur d'initialisation, et le stockage T) plus, si optional contient une valeur, l'utilisation de la mémoire des parties externes (parce que la poignée de T est déjà incluse dans le sizeof).

Maintenant, parce que nous définissons cette surcharge dans le même espace de noms que le type d'argument, nous pouvons mettre cette déclaration après chaque template qui peut l'utiliser, parce que ce sera choisi par l'ADL dans la deuxième phase de la résolution de surcharge. Cependant, nous devons nous assurer que cette surcharge est définie après la surcharge pour std::vector, parce que sinon notre surcharge ne verra pas cette dernière si nous utilisons le framework avec le type boost::optional <std::vector<int>>. À ce point, ça semble assez compliqué du point de vue de l'implémenteur du framework, mais pour les utilisateurs, nous offrons une flexibilité dans l'ordre d'inclusion des en-têtes. Autrement dit, les deux ordres suivants fonctionneront :

 
Sélectionnez
1.
2.
3.
#include <framework.hpp>
#include <boost/optional.hpp>
#include "glue_between_framework_and_optional.hpp"
 
Sélectionnez
1.
2.
3.
#include <boost/optional.hpp>
#include <framework.hpp>
#include "glue_between_framework_and_optional.hpp"

Notez également que, dans la mise en œuvre de la dernière surcharge, j'ai utilisé une using-declaration. Cela pour que la résolution de surcharge considère à la fois l'espace de noms framework et les namespaces en fonction des arguments. Si j'avais oublié ça, j'aurais obtenu une erreur de compilation. De même, si j'appelais juste framework:: mem_usage1 (), j'aurais désactivé l'ADL, et obtenu une autre erreur de compilation dans d'autres cas.

D'ailleurs, toute personne qui veut utiliser nos surcharges de la fonction mem_usage1 devra faire la même chose : mettre une using-declaration, puis les appeler sans qualification d'espace de noms. Afin d'épargner les utilisateurs cette difficulté, nous pouvons fournir une fonction de commodité qui le fait déjà :

 
Sélectionnez
1.
2.
3.
4.
5.
namespace framework
{
  template <typename T>
    size_t mem_usage(const T& v) { return mem_usage1(v); }
}

Parce que je la déclare aussi dans l'espace de noms framework, je peux sauter la using-declaration : elle est implicite. Mais les utilisateurs peuvent désormais l'appeler de manière qualifiée :

 
Sélectionnez
1.
2.
3.
4.
5.
int main()
{
  boost::optional<std::vector<int>> ov;
  framework::mem_usage(ov); // Fonctionne!
}

Pour en revenir à la surcharge de boost::optional, elle fonctionne parce que optional dans la version actuelle de Boost (1.59) est déclaré directement dans l'espace de nom boost :

 
Sélectionnez
1.
2.
3.
4.
5.
namespace boost
{
  template <typename T>
    class optional;
}

S'il était changé en :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
namespace boost
{
  namespace optional_ns
  {        
    template <typename T>
      class optional;
  }
 
  using namespace optional_ns;
}

Ma surcharge cesserait de fonctionner, même si boost::optional fonctionne encore. (Et il y a de bonnes raisons d'ajouter de tels espaces de noms optional_ns supplémentaires, et à un moment donné, ça pourrait bien avoir lieu). Je ne sais pas comment faire pour que ce framework soit préparé pour un tel changement d'espace de noms.

Un autre inconvénient de cette solution à base de surcharge est qu'il est facile de mal épeler le nom de l'une des surcharges. Le compilateur ne protestera pas au point où vous définissez votre framework. Il ne fera que protester lorsque les utilisateurs essayeront de l'utiliser.

Ce choix de conception a été utilisé pour std::swap (et boost::swap est un équivalent du framework::mem_usage de notre exemple). Cependant, notre exemple se différencie de std::swap pour deux raisons. Premièrement, nous ne pouvons nous permettre de définir notre framework dans le namespace std. Deuxièmement, nous ne fournissons pas une implémentation par défaut qui fonctionne pour tout type T pour lequel l'utilisateur n'a pas fourni de personnalisation. De cette façon, nous évitons toute une classe de problèmes de violation d'ODR qui sont présents avec std::swap.

Pour un exemple complet de cette conception, voir ici.

V. Surcharges de fonction avec un tag stimulant l'ADL

Un grand nombre de complications dans la conception précédente provient du fait que nous ne pouvons pas déclarer des surcharges dans le namespace std. Déclarer les surcharges dans les espaces de noms étrangers (comme boost) fonctionne, mais n'est pas robuste si des namespace additionnels viennent s'intercaler (comme boost::optional_ns dans les exemples ci-dessus) ; et c'est aussi un peu confus et inélégant : pourquoi voulons-nous déclarer quelque chose dans l'espace de noms de quelqu'un d'autre ?

Ces problèmes peuvent être évités avec une astuce. Changer l'interface de notre fonction (template), de sorte qu'elle prenne un second argument qui ne change pas dans les surcharges, et est déclaré dans notre espace de noms framework :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
namespace framework
{s
  struct adl_tag {}; // classe vide
 
  // surcharge générale
  template <typename T>
    std::enable_if_t<std::is_trivial<T>::value, size_t>
    mem_usage2(const T& v, adl_tag)
    {
      return sizeof v;
    }
}

Pouvez-vous voir ce qu'on y gagne ?

Si nous définissons maintenant une surcharge pour std::vector dans le namespace framework, et nous l'appelons sans qualifications de portée :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
namespace framework
{
  template <typename T>
    size_t mem_usage2(const std::vector<T>& v, adl_tag tag)
    { 
      size_t ans = sizeof(v);
      for (const T& e : v)
        ans += mem_usage2(e, tag); // pass the tag down
      ans += (v.capacity() - v.size()) * sizeof(T);
      return ans;
    }
}
 
int main()
{
  std::vector<int> v;
  mem_usage2(v, framework::adl_tag{});
}

Ça fonctionne ! Cela fonctionne parce que maintenant nous avons deux arguments dans cette fonction : un du namespace std, l'autre du namespace framework. La deuxième phase de la résolution de surcharge dans les templates (ainsi que la résolution de surcharge en dehors des templates) effectue une recherche dépendant de l'argument. Et parce que nous avons deux arguments, deux namespaces sont inclus dans cette recherche. De cette façon, nous pouvons forcer l'ADL à rechercher dans le namespace framework, quel que soit l'espace de noms dans lequel le premier argument est défini, et comme dans la deuxième phase, nous considérons les surcharges déclarées même après le template qui les appelle, nous ne nous préoccupons pas de l'ordre des surcharges.

Dans une certaine mesure, cela est l'approche adoptée par la bibliothèque Boost.Serialization : elle prévoit que l'un des arguments dans les surcharges soit toujours une « archive » de sérialisation, ce qui correspond à notre ADL-tag, mais parce que l'« archive » a un état significatif, la solution apparaît bien plus naturelle.

Nous pouvons cacher ce tag obscur pour les utilisateurs en définissant une fonction d'enrobage :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
namespace framework
{
  template <typename T>
    size_t mem_usage(const T& v)
    { 
      return mem_usage2(v, adl_tag{});
    }
}

Pour un exemple complet de cette implémentation du framework, voir ici.

VI. Spécialisation de la classe template

Comme nous l'avons vu dans un article précédent, le choix naturel pour ces personnalisations de framework - la spécialisation de fonctions templates - ne fonctionnent pas parce que l'on n'a pas le droit de spécialiser partiellement une fonction template.

Toutefois, cette restriction ne concerne pas les templates de classe, de sorte que nous pourrions aussi bien utiliser ceux-ci. Ça va sembler un peu artificiel, car nous n'avons pas vraiment besoin d'une classe et d'un état, mais il se trouve que ça marche. Nous allons spécialiser et instancier des classes uniquement pour appeler une de leurs fonctions membres statiques.

Donc, première tâche : comment faire une fonction générique qui retournera sizeof (X) pour un type trivialement copiable (ou trivial, en raison des lacunes de GCC) et échouera à compiler pour d'autres types, et tout implémenter à base de classes ?

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
namespace framework
{ 
  template <typename T>
  struct mem_usage3 
  {
    static_assert (std::is_trivial<T>::value, "customize!");
    static size_t get(const T& v) { return sizeof v; }
  };
}

Notre template principal (le moins spécialisé) se lie à tout type ; sauf que pour les types non triviaux il déclenche une erreur de compilation avec static_assert. L'utilisation est un peu maladroite :

 
Sélectionnez
1.
2.
3.
4.
5.
int main()
{
  int i = 0;
  framework::mem_usage3<int>::get(i);
}

Mais encore une fois, nous pouvons l'envelopper dans une fonction utilitaire :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
namespace framework
{
  template <typename T>
  size_t mem_usage(const T& v)
  {
    return framework::mem_usage3<T>::get(v);
  }
} 
 
int main()
{
  int i = 0;
  framework::mem_usage(i);
}

Nous personnalisons ce framework en déclarant des spécialisations (partielles ou complètes) de classes templates. Voici un exemple pour std::pair :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
namespace framework
{
  template <typename T, typename U>
  struct mem_usage3< std::pair<T, U> >
  {
    static size_t get(const std::pair<T, U>& v)
    { 
      return mem_usage3<T>::get(v.first)
           + mem_usage3<U>::get(v.second);
    }
  };
}

La spécialisation pour d'autres types est assez simple : vous le faites exactement dans le même namespace que le template maître. La sécurité supplémentaire apportée par cette technique est que si vous faites une faute d'orthographe en personnalisant le framework, le compilateur enverra immédiatement une erreur de compilation parce que vous aurez spécialisé un modèle de classe inexistant.

Une autre différence importante avec les solutions à base de surcharge est qu'une spécialisation de classe pour X ne fonctionne pas automatiquement pour les types dérivés publiquement de X. Laissez-moi vous expliquer. Si vous avez deux types liés par héritage :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
namespace ns_x
{
  struct X {};
}
 
namespace ns_y
{
  struct Y : ns_x::X {};
}

Et vous définissez une fonction (surcharge) pour X accessible par ADL :

 
Sélectionnez
1.
2.
3.
4.
namespace ns_x
{
  size_t mem_usage1(const X&) { return 1; }
}

Il devient immédiatement accessible par ADL pour Y, même si les deux types se trouvent dans des namespaces non liés :

 
Sélectionnez
1.
2.
3.
4.
5.
int main()
{
  ns_y::Y y;
  mem_usage1(y); // works
}

Cela peut être une caractéristique désirée ou pas en fonction des types X, Y, et la logique de la fonction surchargée, mais quoi qu'il en soit : vous obtenez ce comportement lorsque vous utilisez les techniques à base de surcharge sur la base, mais pas lors de l'utilisation de techniques à base de spécialisation de template de classe.

Pour un exemple complet de cette implémentation du framework, voir ici. Cette technique a été choisie pour std::hash, bien que ce ne soit pas visible au premier abord, car dans le cas de std::hash, une fonction membre non statique est utilisée (l'opérateur d'appel de fonction), ce qui nécessite la création d'un objet temporaire :

 
Sélectionnez
1.
2.
3.
4.
5.
int main()
{
  int i = 0;
  std::hash<int>{}(i);
}

Mais l'idée reste la même.

Cette technique devient un choix attrayant quand un framework nécessite que deux ou plusieurs opérations soient disponibles sur chaque type. La portée de la classe devient un moyen pratique pour grouper les opérations.

VII. Conclusion

Dans toutes ces techniques, il y a un aspect commun : les points de personnalisation (nommés mem_usage1, mem_usage2 et mem_usage3) sont séparés de l'interface exposée : la fonction mem_usage. Cela est une application particulière d'une bonne pratique générale : on sépare l'implémentation des points de spécialisation. Elle s'applique au-delà des templates et de la surcharge. Pour une autre application, voir l'article « Virtuality » de Herb Sutter.

Puisque std::swap peut être utilisé comme point de personnalisation et comme interface pour les utilisateurs, beaucoup de problèmes y sont liés (oublier d'inclure certaines surcharges en fait partie). Il y a des tentatives pour fournir une interface utilisateur et des points de personnalisation distincts, mais avec le même nom, comme proposé par Eric Niebler dans N4381.

Je suis très reconnaissant à Tomasz Kamiński d'avoir partagé ses idées sur le sujet avec moi, et contribué à améliorer ce tutoriel.

VIII. Références

  1. Robert Ramey, Boost.Serialization library.
  2. Herb Sutter, Virtuality.
  3. Eric Niebler, Suggested Design for Customization Points.
  4. Bjørn Reese, Partiality for Functions.

IX. Remerciements

Toute l'équipe de Developpez.com remercie sincèrement Andrzej Krzemieński qui nous a aimablement permis de traduire et de publier son tutoriel sur notre site. Nous tenons également à remercier Laethy pour la traduction, JolyLoic pour la relecture technique et Malick SECK pour la relecture orthographique.

Vous avez aimé ce tutoriel ? Alors partagez-le en cliquant sur les boutons suivants : Viadeo Twitter Facebook Share on Google+   

Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright © 2016 Andrzej Krzemieński. Aucune reproduction, même partielle, ne peut être faite de ce site ni de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.