62 votes

Quelle est la différence entre un trait et une politique ?

J'ai une classe dont j'essaie de configurer le comportement.

template<int ModeT, bool IsAsync, bool IsReentrant> ServerTraits;

Puis, plus tard, j'ai mon objet serveur lui-même :

template<typename TraitsT>
class Server {…};

Ma question est la suivante : mon utilisation ci-dessus est-elle mal nommée ? Mon paramètre modèle est-il en fait une politique au lieu d'un trait ?

Quand un argument modèle est-il un trait ou une politique ?

5 votes

Un trait tend à être une propriété qui est dérivée d'une classe, plutôt que de lui être donnée.

2 votes

Si vous n'êtes pas sûr de vous, vous trouverez de nombreuses informations sur les caractéristiques sur Internet : Une introduction aux Traits C++ , Comment fonctionnent les classes de traits ? , Traits : une nouvelle technique de gabarit utile

0 votes

Donc, ce que j'ai est en fait une politique et non un trait ?

87voto

TemplateRex Points 26447

Politiques

Les politiques sont des classes (ou des modèles de classe) pour comportement d'injection dans une classe mère, généralement par héritage. En décomposant une interface parente en dimensions orthogonales (indépendantes), les classes de politiques forment les éléments constitutifs d'interfaces plus complexes. Un modèle souvent rencontré consiste à fournir des politiques en tant que paramètres de modèles (ou de modèles) définissables par l'utilisateur, avec une valeur par défaut fournie par la bibliothèque. Un exemple tiré de la bibliothèque standard est celui des allocateurs, qui sont des paramètres de modèle de politique pour tous les conteneurs STL.

template<class T, class Allocator = std::allocator<T>> class vector;

Ici, le Allocator (qui est lui-même un modèle de classe !) injecte la politique d'allocation et de désallocation de la mémoire dans la classe mère. std::vector . Si l'utilisateur ne fournit pas d'allocateur, l'allocateur par défaut est le std::allocator<T> est utilisé.

Comme il est typique dans le polymorphisme basé sur les modèles, les exigences d'interface sur les classes de politiques sont les suivantes implicite et sémantique (basée sur des expressions valides) plutôt qu'explicite et syntaxique (basée sur la définition de fonctions membres virtuelles).

Notez que les conteneurs associatifs non ordonnés, plus récents, ont plus d'une politique. En plus de la politique habituelle Allocator ils prennent également un paramètre de modèle Hash qui a pour valeur par défaut std::hash<Key> objet fonction. Cela permet aux utilisateurs de conteneurs non ordonnés de les configurer selon plusieurs dimensions orthogonales (allocation de mémoire et hachage).

Traits

Les traits sont des modèles de classe pour propriétés des extraits à partir d'un type générique. Il existe deux types de traits : les traits à valeur unique et les traits à valeurs multiples. Les exemples de traits à valeur unique sont ceux de l'en-tête <type_traits>

template< class T >
struct is_integral
{
    static const bool value /* = true if T is integral, false otherwise */;
    typedef std::integral_constant<bool, value> type;
};

Les traits à valeur unique sont souvent utilisés dans modèle-métaprogrammation et les astuces SFINAE pour surcharger un modèle de fonction basé sur une condition de type.

Des exemples de traits à valeurs multiples sont les itérateurs_traits et les allocateurs_traits des en-têtes. <iterator> y <memory> respectivement. Comme les traits sont des modèles de classe, ils peuvent être spécialisés. Voici un exemple de spécialisation de iterator_traits para T*

template<T>
struct iterator_traits<T*>
{
    using difference_type   = std::ptrdiff_t;
    using value_type        = T;
    using pointer           = T*;
    using reference         = T&;
    using iterator_category = std::random_access_iterator_tag;
};

L'expression std::iterator_traits<T>::value_type permet de créer du code générique pour des classes d'itérateurs à part entière, utilisable même pour les pointeurs bruts (puisque les pointeurs bruts n'ont pas de membre value_type ).

Interaction entre les politiques et les traits

Lorsque vous écrivez vos propres bibliothèques génériques, il est important de penser aux façons dont les utilisateurs peuvent spécialiser vos propres modèles de classe. Il faut cependant faire attention à ce que les utilisateurs ne soient pas victimes de l'approche de la Règle de la définition unique en utilisant des spécialisations de traits pour injecter plutôt que pour extraire des comportements. Pour paraphraser ceci vieux poste par Andrei Alexandrescu

Le problème fondamental est que le code qui ne voit pas la version spécialisée spécialisée d'un trait compile quand même, est susceptible d'être lié, et parfois même s'exécuter. Ceci est dû au fait qu'en l'absence de la spécialisation explicite, le modèle non-spécialisé se met en place, implémentant probablement implémentant un comportement générique qui fonctionne aussi pour votre cas spécial. spécial. Par conséquent, si tout le code d'une application ne voit pas la même même définition d'un trait, l'ODR est violée.

Le C++11 std::allocator_traits évite ces écueils en imposant que tous les conteneurs STL ne puissent extraire des propriétés que de leur conteneur STL. Allocator les politiques par std::allocator_traits<Allocator> . Si les utilisateurs choisissent de ne pas fournir ou d'oublier de fournir certains des membres requis de la politique, la classe de traits peut intervenir et fournir des valeurs par défaut pour ces membres manquants. Parce que allocator_traits lui-même ne peut pas être spécialisé, les utilisateurs doivent toujours passer une politique d'allocation entièrement définie afin de personnaliser l'allocation de mémoire de leurs conteneurs, et aucune violation silencieuse de l'ODR ne peut se produire.

Notez qu'en tant que rédacteur de bibliothèque, on peut toujours spécialiser les modèles de classe de traits (comme la STL le fait dans iterator_traits<T*> ), mais c'est une bonne pratique de passer toutes les spécialisations définies par l'utilisateur à travers les classes de politique dans des traits multivalués qui peuvent extraire le comportement spécialisé (comme le STL le fait dans allocator_traits<A> ).

UPDATE : Les problèmes d'ODR des spécialisations définies par l'utilisateur des classes de traits se produisent principalement lorsque les traits sont utilisés comme modèles de classe globale et vous ne pouvez pas garantir que tous les futurs utilisateurs verront toutes les autres spécialisations définies par l'utilisateur. Les politiques sont paramètres de modèles locaux et contiennent toutes les définitions pertinentes, ce qui leur permet d'être définies par l'utilisateur sans interférence dans d'autres codes. Les paramètres locaux des modèles qui ne contiennent que des types et des constantes - mais pas de fonctions comportementales - pourraient toujours être appelés "traits", mais ils ne seraient pas visibles pour le reste du code, comme la fonction std::iterator_traits y std::allocator_traits .

0 votes

Toujours pour approfondir la question, j'ai trouvé ce qui suit dans l'article original de Meyers ( cantrip.org/traits.html ) "Classe de traits : Une classe utilisée à la place des paramètres des modèles. En tant que classe, elle agrège des types et des constantes utiles ; en tant que modèle, elle fournit une voie pour ce "niveau supplémentaire d'indirection" qui résout tous les problèmes logiciels". Dans mon exemple original, j'ai une agrégation de politiques. Est-ce que cela en fait un trait alors ? Mais il s'agit toujours d'injecter un comportement, ce qui lui donne une caractéristique de politique ! Il semble que ce soit les deux. Argh, j'ai encore des problèmes pour savoir comment l'appeler. Entrée ?

1 votes

@NateDoromal Une politique peut également contenir des types et des constantes, mais elle est généralement plus orientée vers le comportement. Une autre différence est que les politiques sont transmises en tant que paramètres de modèle, tandis que les traits sont des classes globales (c'est la raison pour laquelle c'est une mauvaise idée pour les utilisateurs de les spécialiser, car vous ne savez jamais quels autres utilisateurs verront quelles spécialisations, ce qui conduit aux problèmes ODR mentionnés). Parce que vous passez TraitsT comme paramètre, j'appellerais votre TraitsT une politique. Si vous disposez également d'une classe globale pour extraire les différents types et fonctions, il s'agit alors d'une classe de traits multivalents.

4 votes

NateDoromal N'oubliez pas non plus que les concepts de traits et de politiques ont été développés il y a plus de 15 ans. Depuis lors, une expérience substantielle a été acquise à leur sujet. Les problèmes d'ODR qu'Alexandrescu a identifiés ont été évités dans les récents traits d'allocateurs et d'itérateurs de la STL. La nomenclature n'est pas imposée par la norme, alors gardez à l'esprit les différences sémantiques plutôt que la vieille distinction comportement/type/constant. Politiques : injection, local, personnalisable ; Traits : extraction, global, fixe (pour éviter l'ODR).

25voto

Andy Prowl Points 62121

Je pense que vous trouverez la meilleure réponse possible à votre question dans ce livre d'Andrei Alexandrescu . Je vais essayer de vous en donner un bref aperçu. J'espère que cela vous aidera.


A classe de traits est une classe qui est généralement destinée à être une métafonction associant des types à d'autres types ou à des valeurs constantes pour fournir une caractérisation de ces types. En d'autres termes, il s'agit d'un moyen de modéliser les propriétés des types . Le mécanisme exploite normalement les modèles et la spécialisation des modèles pour définir l'association :

template<typename T>
struct my_trait
{
    typedef T& reference_type;
    static const bool isReference = false;
    // ... (possibly more properties here)
};

template<>
struct my_trait<T&>
{
    typedef T& reference_type;
    static const bool isReference = true;
    // ... (possibly more properties here)
};

La métafonction trait my_trait<> ci-dessus associe le type de référence T& et la valeur booléenne constante false à tous les types T qui sont no eux-mêmes des références ; d'autre part, il associe le type de référence T& et la valeur booléenne constante true à tous les types T que sont références.

Ainsi, par exemple :

int  -> reference_type = int&
        isReference = false

int& -> reference_type = int&
        isReference = true

En code, nous pourrions affirmer ce qui précède comme suit (les quatre lignes ci-dessous compileront, ce qui signifie que la condition exprimée dans le premier argument à static_assert() est satisfaite) :

static_assert(!(my_trait<int>::isReference), "Error!");
static_assert(  my_trait<int&>::isReference, "Error!");
static_assert(
    std::is_same<typename my_trait<int>::reference_type, int&>::value, 
    "Error!"
     );
static_assert(
    std::is_same<typename my_trait<int&>::reference_type, int&>::value, 
    "Err!"
    );

Ici, vous pouvez voir que j'ai utilisé le standard std::is_same<> qui est lui-même une méta-fonction acceptant deux plutôt qu'un seul argument de type. Les choses peuvent devenir arbitrairement compliquées ici.

Bien que std::is_same<> fait partie de la type_traits certains considèrent qu'un modèle de classe n'est une classe à traits de type que s'il agit comme un méta-prédicat (acceptant donc un paramètre de modèle). À ma connaissance, cependant, la terminologie n'est pas clairement définie.

Pour un exemple d'utilisation d'une classe de traits dans la bibliothèque standard C++, regardez comment sont conçues les bibliothèques Input/Output et String.


A politique est quelque chose de légèrement différent (en fait, assez différent). Il s'agit normalement d'une classe qui spécifie le comportement d'une autre classe générique concernant certaines opérations qui peuvent être réalisées de différentes manières (et dont la mise en œuvre est, par conséquent, laissée à la classe de politique).

Par exemple, une classe générique de pointeurs intelligents pourrait être conçue comme une classe modèle acceptant une politique comme paramètre de modèle pour décider de la manière de gérer le comptage des références. Il ne s'agit que d'un exemple hypothétique, excessivement simpliste et illustratif, alors essayez de faire abstraction de ce code concret et de vous concentrer sur l'objectif de la classe. mécanisme .

Cela permettrait au concepteur du pointeur intelligent de ne pas prendre d'engagement codé en dur quant à savoir si les modifications du compteur de référence doivent être effectuées de manière sûre pour les threads :

template<typename T, typename P>
class smart_ptr : protected P
{
public:
    // ... 
    smart_ptr(smart_ptr const& sp)
        :
        p(sp.p),
        refcount(sp.refcount)
    {
        P::add_ref(refcount);
    }
    // ...
private:
    T* p;
    int* refcount;
};

Dans un contexte multithread, un client pourrait utiliser une instanciation du modèle de pointeur intelligent avec une politique qui réalise des incréments et des décréments du compteur de référence à l'abri des threads (plateforme Windows supposée ici) :

class mt_refcount_policy
{
protected:
    add_ref(int* refcount) { ::InterlockedIncrement(refcount); }
    release(int* refcount) { ::InterlockedDecrement(refcount); }
};

template<typename T>
using my_smart_ptr = smart_ptr<T, mt_refcount_policy>;

Dans un environnement monofilaire, par contre, un client pourrait instancier le modèle de pointeur intelligent avec une classe de politique qui augmente et diminue simplement la valeur du compteur :

class st_refcount_policy
{
protected:
    add_ref(int* refcount) { (*refcount)++; }
    release(int* refcount) { (*refcount)--; }
};

template<typename T>
using my_smart_ptr = smart_ptr<T, st_refcount_policy>;

De cette façon, le concepteur de la bibliothèque a fourni une solution flexible, capable d'offrir le meilleur compromis entre performance et sécurité ( "Vous ne payez pas pour ce que vous n'utilisez pas". ).

2 votes

Le typedef de reference_type est incorrect pour my_traits<T&> il devrait être typedef T& reference_type; .

0 votes

@SylvainDefresne : Bien, c'est corrigé. Merci

0 votes

Vous pouvez en fait utiliser une politique de suppression avec des pointeurs intelligents.

3voto

Twisted Oracle Points 26

Si vous utilisez ModeT, IsReentrant et IsAsync pour contrôler le comportement du serveur, alors il s'agit d'une politique.

Par ailleurs, si vous souhaitez décrire les caractéristiques du serveur à un autre objet, vous pouvez définir une classe de traits comme suit :

template <typename ServerType>
class ServerTraits;

template<>
class ServerTraits<Server>
{
    enum { ModeT = SomeNamespace::MODE_NORMAL };
    static const bool IsReentrant = true;
    static const bool IsAsync = true;
}

0 votes

Bien, cela résume en fait la réponse acceptée à l'exemple donné :)

1voto

jmetcalfe Points 843

Voici quelques exemples pour clarifier le commentaire d'Alex Chamberlain :

Un exemple courant de classe de traits est std::iterator_traits. Disons que nous avons une classe modèle C avec une fonction membre qui prend deux itérateurs, itère sur les valeurs et accumule le résultat d'une certaine manière. Nous voulons que la stratégie d'accumulation soit également définie comme faisant partie du modèle, mais nous utiliserons une politique plutôt qu'un trait pour y parvenir.

template <typename Iterator, typename AccumulationPolicy>
class C{
    void foo(Iterator begin, Iterator end){
        AccumulationPolicy::Accumulator accumulator;
        for(Iterator i = begin; i != end; ++i){
            std::iterator_traits<Iterator>::value_type value = *i;
            accumulator.add(value);
        }
    }
};

La politique est transmise à notre classe modèle, tandis que le trait est dérivé du paramètre du modèle. Ce que vous obtenez est donc plus proche d'une politique. Il existe des situations où les traits sont plus appropriés, et d'autres où les politiques sont plus appropriées, et souvent le même effet peut être obtenu avec l'une ou l'autre méthode, ce qui donne lieu à un débat sur la méthode la plus expressive.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X