web-dev-qa-db-fra.com

Polymorphisme statique C++ (CRTP) et utilisation de typedefs à partir de classes dérivées

J'ai lu l'article Wikipedia sur le modèle de modèle curieusement récurrent en C++ permettant de réaliser un polymorphisme statique (lire: au moment de la compilation). Je voulais le généraliser afin de pouvoir modifier les types de retour des fonctions en fonction du type dérivé. (Cela semble être possible car le type de base connaît le type dérivé à partir du paramètre template). Malheureusement, le code suivant ne compilera pas avec MSVC 2010 (je n’ai pas un accès facile à gcc pour l’instant, je ne l’ai donc pas encore essayé). Quelqu'un sait pourquoi?

template <typename derived_t>
class base {
public:
    typedef typename derived_t::value_type value_type;
    value_type foo() {
        return static_cast<derived_t*>(this)->foo();
    }
};

template <typename T>
class derived : public base<derived<T> > {
public:
    typedef T value_type;
    value_type foo() {
        return T(); //return some T object (assumes T is default constructable)
    }
};

int main() {
    derived<int> a;
}

En passant, j'ai un moyen de contourner le problème en utilisant des paramètres de modèle supplémentaires, mais je ne l'aime pas - cela deviendra très verbeux lors du passage de nombreux types dans la chaîne d'héritage.

template <typename derived_t, typename value_type>
class base { ... };

template <typename T>
class derived : public base<derived<T>,T> { ... };

EDIT:

Le message d'erreur que MSVC 2010 envoie dans cette situation est error C2039: 'value_type' : is not a member of 'derived<T>'

g ++ 4.1.2 (via codepad.org ) dit error: no type named 'value_type' in 'class derived<int>'

49
Samuel Powell

derived est incomplet lorsque vous l'utilisez comme argument de modèle pour base dans sa liste de classes de base.

Une solution de contournement courante consiste à utiliser un modèle de classe de traits. Voici votre exemple, classé. Cela montre comment utiliser les types et les fonctions de la classe dérivée via les traits.

// Declare a base_traits traits class template:
template <typename derived_t> 
struct base_traits;

// Define the base class that uses the traits:
template <typename derived_t> 
struct base { 
    typedef typename base_traits<derived_t>::value_type value_type;
    value_type base_foo() {
        return base_traits<derived_t>::call_foo(static_cast<derived_t*>(this));
    }
};

// Define the derived class; it can use the traits too:
template <typename T>
struct derived : base<derived<T> > { 
    typedef typename base_traits<derived>::value_type value_type;

    value_type derived_foo() { 
        return value_type(); 
    }
};

// Declare and define a base_traits specialization for derived:
template <typename T> 
struct base_traits<derived<T> > {
    typedef T value_type;

    static value_type call_foo(derived<T>* x) { 
        return x->derived_foo(); 
    }
};

Vous devez juste spécialiser base_traits pour tous les types que vous utilisez pour l'argument de modèle derived_t de base et vous assurer que chaque spécialisation fournit tous les membres requis par base.

52
James McNellis

Un petit inconvénient de l'utilisation des traits est que vous devez en déclarer un pour chaque classe dérivée. Vous pouvez écrire une solution de contournement moins verbeuse et redondante comme ceci:

template <template <typename> class Derived, typename T>
class base {
public:
    typedef T value_type;
    value_type foo() {
        return static_cast<Derived<T>*>(this)->foo();
    }
};

template <typename T>
class Derived : public base<Derived, T> {
public:
    typedef T value_type;
    value_type foo() {
        return T(); //return some T object (assumes T is default constructable)
    }
};

int main() {
    Derived<int> a;
}
8
matovitch

En C++ 14, vous pouvez supprimer la typedef et utiliser la fonction auto type de retour déduction:

template <typename derived_t>
class base {
public:
    auto foo() {
        return static_cast<derived_t*>(this)->foo();
    }
};

Cela fonctionne car la déduction du type de retour de base::foo est retardée jusqu'à ce que derived_t soit terminé.

6
Oktalist

Une alternative aux caractères de type qui nécessite moins de passe-partout consiste à imbriquer votre classe dérivée dans une classe wrapper contenant vos typedefs (ou using) et à passer le wrapper comme argument de modèle à votre classe de base.

template <typename Outer>
struct base {
    using derived = typename Outer::derived;
    using value_type = typename Outer::value_type;
    value_type base_func(int x) {
        return static_cast<derived *>(this)->derived_func(x); 
    }
};

// outer holds our typedefs, derived does the rest
template <typename T>
struct outer {
    using value_type = T;
    struct derived : public base<outer> { // outer is now complete
        value_type derived_func(int x) { return 5 * x; }
    };
};

// If you want you can give it a better name
template <typename T>
using NicerName = typename outer<T>::derived;

int main() {
    NicerName<long long> obj;
    return obj.base_func(5);
}
2
Alkis

Je sais que c'est en gros la solution de contournement que vous avez trouvée et que vous n'aimez pas, mais je voulais la documenter et aussi dire que c'est fondamentalement la solution actuelle à ce problème.

Je cherchais un moyen de faire cela depuis un moment et je n’avais jamais trouvé une bonne solution. C’est impossible parce que, finalement, des choses comme boost::iterator_facade<Self, different_type, value_type, ...> nécessitent de nombreux paramètres.

Bien sûr, nous aimerions que quelque chose comme cela fonctionne:

template<class CRTP> 
struct incrementable{
    void operator++(){static_cast<CRTP&>(*this).increment();}
    using ptr_type = typename CRTP::value_type*; // doesn't work, A is incomplete
};

template<class T>
struct A : incrementable<A<T>>{
    void increment(){}
    using value_type = T;
    value_type f() const{return value_type{};}
};

int main(){A<double> a; ++a;}

Si cela était possible, tous les traits de la classe dérivée pourraient être passés implicitement à la classe de base. L'idiome que j'ai trouvé pour obtenir le même effet est de transmettre entièrement les traits à la classe de base.

template<class CRTP, class ValueType> 
struct incrementable{
    void operator++(){static_cast<CRTP&>(*this).increment();}
    using value_type = ValueType;
    using ptr_type = value_type*;
};

template<class T>
struct A : incrementable<A<T>, T>{
    void increment(){}
    typename A::value_type f() const{return typename A::value_type{};}
//    using value_type = typename A::value_type;
//    value_type f() const{return value_type{};}
};

int main(){A<double> a; ++a;}

https://godbolt.org/z/2G4w7d

L'inconvénient est que vous devez accéder à la caractéristique de la classe dérivée avec un typename qualifié ou un réactivé de using

0
alfC