web-dev-qa-db-fra.com

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

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

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

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

template<typename TraitsT>
class Server {...};

Ma question est pour mon utilisation ci-dessus mon nom est-il mal nommé? Mon paramètre basé sur un modèle est-il en fait une politique plutôt qu'un trait?

Quand un modèle d'argument est-il un trait par rapport à une politique?

59
Nathan Doromal

Stratégies

Les stratégies sont des classes (ou des modèles de classe) pour injecter le comportement dans une classe parent, généralement par héritage. En décomposant une interface parent en dimensions orthogonales (indépendantes), les classes de règles forment les blocs de construction d'interfaces plus complexes. Un modèle souvent observé consiste à fournir des stratégies en tant que paramètres de modèle définissables par l'utilisateur (ou modèle de modèle) avec une valeur par défaut fournie par la bibliothèque. Un exemple de la bibliothèque standard sont les allocateurs, qui sont des paramètres de modèle de politique de tous les conteneurs STL

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

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

Comme c'est typique dans le polymporphisme basé sur un modèle, les exigences d'interface sur les classes de politique sont implicites et sémantiques (basées sur des expressions valides) plutôt qu'explicites et syntaxiques (basées sur la définition des fonctions de membre virtuel).

Notez que les conteneurs associatifs non ordonnés les plus récents ont plusieurs stratégies. En plus du paramètre de modèle Allocator habituel, ils prennent également une politique Hash qui prend par défaut std::hash<Key> objet de 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 extraire les propriétés d'un type générique. Il existe deux types de traits: les traits à valeur unique et les traits à valeurs multiples. Des 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 la métaprogrammation de modèle 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 iterator_traits et allocator_traits des en-têtes <iterator> et <memory>, respectivement. Comme les traits sont des modèles de classe, ils peuvent être spécialisés. Ci-dessous un exemple de spécialisation de iterator_traits pour 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 rendre le code générique pour les 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 politiques et traits

Lors de l'écriture de vos propres bibliothèques génériques, il est important de réfléchir aux façons dont les utilisateurs peuvent spécialiser vos propres modèles de classe. Il faut cependant faire attention à ne pas laisser les utilisateurs être victimes de la règle de définition unique en utilisant des spécialisations de traits pour injecter plutôt que pour extraire le comportement. Pour paraphraser cela ancien poste par Andrei Alexandrescu

Le problème fondamental est que le code qui ne voit pas la version spécialisée d'un trait sera toujours compilé, est susceptible de se lier et peut même parfois s'exécuter. En effet, en l'absence de spécialisation explicite, le modèle non spécialisé entre en jeu, implémentant probablement un comportement générique qui fonctionne également pour votre cas spécial. Par conséquent, si tout le code d'une application ne voit pas la même définition d'un trait, l'ODR est violé.

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

Notez qu'en tant qu'écrivain de bibliothèque, on peut toujours spécialiser les modèles de classe de traits (comme le fait la STL dans iterator_traits<T*>), mais il est recommandé de passer toutes les spécialisations définies par l'utilisateur via des classes de règles en traits à valeurs multiples qui peuvent extraire le comportement spécialisé (comme le fait la STL dans allocator_traits<A>).

[~ # ~] update [~ # ~] : Les problèmes ODR des spécialisations définies par l'utilisateur des classes de traits surviennent 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 des paramètres de modèle 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 de modèle local qui ne contiennent que le type et les constantes - mais pas de fonctions comportementales - pourraient toujours être appelés "traits", mais ils ne seraient pas visibles par d'autres codes comme le std::iterator_traits et std::allocator_traits.

84
TemplateRex

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


Une classe de traits est une classe qui est généralement destinée à être une méta-fonction associant des types à d'autres types ou à des valeurs constantes pour fournir une caractérisation de ces types. En d'autres termes, c'est une façon de modéliser 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 pas eux-mêmes 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 qui sont références.

Ainsi, par exemple:

int  -> reference_type = int&
        isReference = false

int& -> reference_type = int&
        isReference = true

Dans le code, nous pourrions affirmer ce qui précède comme suit (les quatre lignes ci-dessous seront compilées, ce qui signifie que la condition exprimée dans le premier argument de 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 modèle standard std::is_same<>, Qui est lui-même une méta-fonction qui accepte deux, plutôt qu'un seul argument de type. Les choses peuvent devenir arbitrairement compliquées ici.

Bien que std::is_same<> Fasse partie de l'en-tête type_traits, Certains considèrent un modèle de classe comme une classe de traits de type uniquement s'il agit comme un méta-prédicat (donc, en acceptant 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 la bibliothèque d'entrée/sortie et la bibliothèque de chaînes sont conçues.


Une politique est quelque chose de légèrement différent (en fait, assez différent). Elle est normalement censée être une classe qui spécifie le comportement d'une autre classe générique à l'égard de certaines opérations qui pourraient être potentiellement réalisées de plusieurs manières différentes (et dont l'implémentation est, par conséquent, laissée à la classe de politique).

Par exemple, une classe générique de pointeur intelligent pourrait être conçue comme une classe de modèle qui accepte une politique comme paramètre de modèle pour décider comment gérer le comptage de références - ceci est juste un exemple hypothétique, trop simpliste et illustratif, veuillez donc essayer d'abstraire à partir de ce code concret et se concentrer sur le mécanisme .

Cela permettrait au concepteur du pointeur intelligent de ne pas prendre d'engagement codé en dur pour savoir si les modifications du compteur de référence doivent être effectuées de manière sécurisée 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 multi-thread, un client peut utiliser une instanciation du modèle de pointeur intelligent avec une stratégie qui réalise des incréments et des diminutions thread-safe du compteur de référence (plate-forme 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 à thread unique, d'un autre côté, un client peut instancier le modèle de pointeur intelligent avec une classe de stratégie 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 bibliothèque a fourni une solution flexible qui est capable d'offrir le meilleur compromis entre performance et sécurité ( "Vous ne payez pas pour ce que vous n'utilisez pas").

24
Andy Prowl

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

Alternativement, si vous voulez un moyen de décrire les caractéristiques du serveur à un autre objet, vous pouvez définir une classe de traits comme ceci:

template <typename ServerType>
class ServerTraits;

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

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

Un exemple courant d'une classe de traits est std :: iterator_traits. Disons que nous avons une classe de modèle C avec une fonction membre qui prend deux itérateurs, itère sur les valeurs et accumule le résultat d'une manière ou d'une autre. Nous voulons que la stratégie d'accumulation soit définie comme faisant également 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 stratégie est transmise à notre classe de modèle, tandis que la caractéristique est dérivée du paramètre de modèle. Donc, ce que vous avez ressemble davantage à une politique. Il y a des situations où les traits sont plus appropriés, et où les politiques sont plus appropriées, et souvent le même effet peut être obtenu avec l'une ou l'autre méthode conduisant à un débat sur ce qui est le plus expressif.

1
jmetcalfe