2017-01-12 6 views
1

Quelqu'un peut-il suggérer une technique pour éviter les fonctions de modèle virtuel dans le code suivant? J'ai lu plusieurs autres articles, et je ne vois pas comment appliquer ces solutions à ce cas.Éviter les fonctions de modèle virtuel

Je construis une bibliothèque qui contient une hiérarchie de classes modélisées. Je veux créer un tableau de fonctions "usine" qui peuvent être utilisées pour instancier des classes dérivées par nom (par exemple, en fonction des paramètres de ligne de commande). Dans la mesure du possible, je souhaite que chaque classe dérivée puisse s'enregistrer dans son propre fichier .hpp ou .cpp (plutôt que de devoir conserver une seule liste de toutes les classes dérivées possibles quelque part).

Le code ci-dessous presque fonctionne, à l'exception de la défaillance fatale d'essayer d'utiliser une fonction de modèle virtuel.

// 
// This code would appear in a library 
// 

template<class T> 
class Base { 
public: 
    Base(const char* /*param*/) {} 
}; 

// 
// Description of each derived class. 
// We need this non-templated base class so we can store all our 
// descriptions in a single vector 
// 
class DescriptionBase { 

private: 
    const char *description; 
    const char *name; 

public: 
    DescriptionBase(const char* pDesc, const char* pName) : description(pDesc), name(pName){ 
    // Whenever a Description object is created, it is automatically registered with the 
    // global descriptionList. This allows us to register derived classes in their own 
    // .cpp/.hpp file (as opposed to keeping a centralized list of all derived classes). 
    descriptionList.push_back(this); 
    } 

    // FAIL Can't have virtual template functions 
    virtual template<class T> 
    Base<T> *make(const char *param) {return new Base<T>(param); } 

    static vector<DescriptionBase *> descriptionList; 
}; 

//global list of all derived classes 
vector<DescriptionBase *> DescriptionBase::descriptionList; 

// We use the template to store the type of the derived class 
// for use in the make method 
template<template<typename> class D> 
class Description : public DescriptionBase { 

public: 
    Description(const char* pDesc, const char* pName) : DescriptionBase(pDesc, pName) {} 

    template<class T> 
    Base<T> *make(const char *params) { 
    return new D<T>(params); 
    } 
}; 

// 
// These derived classes may be part of the library, or they may be 
// written by users of the library. 
// 


template<class T> 
class DerivedA : public Base<T> { 
public: 
    DerivedA(const char* param) : Base<T>(param) {return;} 
}; 

Description<DerivedA> derivedA("derivedA", "This is the first derived class"); 

template<class T> 
class DerivedB : public Base<T> { 
    DerivedB(const char* param) : Base<T>(param) {return;} 
}; 
Description<DerivedA> derivedB("derivedA", "This is the second derived class"); 

// 
// Example code written by the user of the library. 
// 
// 


int main(int argc, char *argv[]) { 

    // Using a descriptionList is just a short-cut here. 
    // Final code will use a map. 
    int indexOfDerivedA = 0; 

    Base<int> *intItem = DescriptionBase::descriptionList[indexOfDerivedA]->make<int>("parameter to derived type's constructor"); 
    Base<char> *charItem = DescriptionBase::descriptionList[indexOfDerivedA]->make<char>("parameter to derived type's constructor"); 


} 
+1

Si vous ne connaissez pas le type de la valeur de retour, vous ne pouvez pas utiliser le constructeur de manière judicieuse, donc ils doivent avoir un type de retour commun (donc vous pouvez réellement l'affecter à quelque chose) ou ils doivent être usines nommées séparément. Le système de type ne peut pas savoir quel type est un "fred" ou un "george", mais il doit déterminer au moment de la compilation si l'appel est valide. Il est important de se rappeler que Base et Base ne sont aucunement liés au point de vue du système de type. – xaxxon

+0

Je n'ai pas lu votre code mais vous pouvez le faire en ayant une carte de chaînes pour les foncteurs qui retournent un objet correspondant à une interface. Les classes s'enregistrent en insérant un foncteur dans cette carte. – imreal

+0

@xaxxon J'ai modifié le code pour être plus clair. "fred" et "george" étaient des paramètres pour le constructeur, pas les noms de type. Base et base ne sont pas destinés à être liés. Je suis resté bloqué au moment de construire le 'descriptionList' dans la bibliothèque (qui, bien sûr, ne sait pas quels paramètres de modèle les utilisateurs choisiront). – Zack

Répondre

2

C'est une balise de compilation et tapez:

template<class T> 
struct tag_t { constexpr tag_t() {}; }; 
template<class T> constexpr tag_t tag<T>{}; 

Voici une liste des types:

template<class...>struct types_t{constexpr types_t(){}; using type=types_t;}; 
template<class...Ts>constexpr types_t<Ts...> types{}; 

Cette mappe le contenu d'une liste de types:

template<template<class...>class Z, class types> 
struct fmap{}; 
template<template<class...>class Z, class types> 
using fmap_t=typename fmap<Z,types>::type; 
template<template<class...>class Z, template<class...>class types, class...Ts> 
struct fmap<Z,types<Ts...>> { 
    using type=types<Z<Ts...>>; 
}; 

Maintenant, faisons une liste d'usines:

template<class...Args> 
struct build_tagged_sig { 
    template<class T> 
    using result = std::unique_ptr<T>(tag_t<T>,Args...); 
}; 
template<template<class...>class Out, class types, class...Args> 
using tagged_factories = 
    fmap_t< 
    std::function, 
    fmap_t< 
     build_tagged_sig<Args...>::template result, 
     fmap_t< Out, types > 
    > 
    >; 

Cela applique un types à un modèle:

template<template<class...>class Z, class types> 
struct apply_types {}; 
template<template<class...>class Z, class types> 
using apply_types_t = typename apply_types<Z,types>::type; 
template<template<class...>class Z, template<class...>class types, class...Ts> 
struct apply_types<Z, types<Ts...>> { 
    using type=Z<Ts...>; 
}; 
template<template<class...>class Z> 
struct applier { 
    template<class types> 
    using result=apply_types_t<Z,types>; 
}; 

This SO post shows how to overload multiple lambdas or std::functions.

using my_types = types_t< std::int8_t, std::int16_t, std::int32_t, std::int64_t >; 

using magic_factory = 
    apply_types_t< 
    overload, 
    tagged_factories< Base, my_types, const char* > > 
    >; 

qui est une surcharge sur

std::function< std::unique_ptr<Base<std::int8_t>>(tag_t<Base<std::int8_t>>, const char*) >, 
std::function< std::unique_ptr<Base<std::int16_t>>(tag_t<Base<std::int16_t>>, const char*) >, 
std::function< std::unique_ptr<Base<std::int32_t>>(tag_t<Base<std::int32_t>>, const char*) >, 
std::function< std::unique_ptr<Base<std::int64_t>>(tag_t<Base<std::int64_t>>, const char*) > 

Nous écrivons maintenant une usine de registre:

template<class F, class...Ts> 
magic_factory make_poly_factory(types_t<Ts...>, F&& f) { 
    return magic_factory(
    (void(tag<Ts>), f)... 
); 
} 
template<class F> 
magic_factory make_poly_factory(F&& f) { 
    return make_poly_factory(my_types{}, f); 
} 

qui crée des copies N de f et stocke chacun dans un std::function en un seul objet. En prenant la valeur de retour, vous pouvez appeler un individu par une résolution de surcharge.

template<class T> 
std::unique_ptr<Base<T>> factory_A_impl(tag_t<Base<T>>, const char* param) { 
    return new DerivedA<T>(param); 
} 
auto magic = magic_factory(my_types{}, [](auto tag, const char* param){ 
    return factory_A_impl(tag, param); 
}); 

std::unique_ptr<Base<std::int8_t>> bob = magic(tag<std::int8_t>, "hello"); 

et bob est un unique_ptr à un Base<std::int8_t> qui est en fait un DerivedA<std::int8_t> lors de l'exécution.

Ceci a probablement des tpyos.

La plupart de ce post était la métaprogrammation pour configurer l'objet unique qui surcharge chacun des tag_t<T0> à tag_t<T1> sans me répéter. Vous pouvez le faire manuellement pour vos 4 types. La surcharge que j'ai utilisée suppose que nous ne prenons pas un seul lambda avec des arguments de template et un type qui efface chaque surcharge, mais un ensemble de lambdas. Le premier ferait quelques choses au-dessus d'un contact plus facile.

L'étudiant de l'utilisateur final a juste pour créer un objet de fonction qui prend un tag_t<X> et un const char* et retourne un unique_ptr<X> et ne coûte pas cher à copier, puis créer un magic_factory de celui-ci (qui type efface en une liasse de std::function s).

DescriptionBase devient:

struct Description { 
    char const* description = 0; 
    char const* name = 0; 
    magic_factory factory; 
    template<class T> 
    std::unique_ptr<Base<T>> 
    make(const char *param) { 
    return factory(tag<T>, param); 
    } 
}; 

Le polymorphisme est maintenant à magic_factory. Stockez les instances de Description, pas de pointeurs sur elles, car elles sont un polymorphisme de type valeur.

Voir, facile. L'ajout de plusieurs paramètres de modèle ajoute simplement un peu plus de complexité aux choses fmap, et plus de responsabilités au créateur de la magic_factory.

Vous devez effectuer une opération inter-produits pour générer les 64 ensembles de types différents à partir de la liste de 4 éléments. Ce sera un types_t de types_t.

Appelez cela my_many_types.

Puis

using magic_factory = 
    apply_types_t< 
    overload, 
    tagged_factories< applier<Base>::template result, my_types, const char* > > 
    >; 

et fait, nous avons maintenant 64 avec des signatures comme les surcharges:

std::unique_ptr<Base<std::int8_t, std::int16_t, std::int8_t>>(
    tag_t<Base<std::int8_t, std::int16_t, std::int8_t>>, 
    const char* 
) 

Maintenant, nous pourrions tout simplement faire tout cela manuellement.

Construire une table comme ceci:

template<class T> 
using factory_ptr = std::unique_ptr<T>(void*, tag_t<T>, const char*); 

using factory_table = std::tuple< 
    factory_ptr< Base< std::int8_t, std::int8_t, std::int8_t > >, 
    factory_ptr< Base< std::int8_t, std::int8_t, std::int16_t > >, 
    factory_ptr< Base< std::int8_t, std::int8_t, std::int32_t > >, 
    factory_ptr< Base< std::int8_t, std::int8_t, std::int64_t > >, 
    factory_ptr< Base< std::int8_t, std::int16_t, std::int8_t > >, 
    factory_ptr< Base< std::int8_t, std::int16_t, std::int16_t > >, 
    factory_ptr< Base< std::int8_t, std::int16_t, std::int32_t > >, 
    factory_ptr< Base< std::int8_t, std::int16_t, std::int64_t > >, 

...

factory_ptr< Base< std::int64_t, std::int64_t, std::int64_t > > 
>; 

maintenant une usine magique:

struct magic_factory { 
    std::unique_ptr<void, void(*)(void*)> state; 
    factory_table table; 

    template<class T0, class T1, class T2> 
    std::unique_ptr<Base<T0, T1, T2>> make(char const* param) { 
    auto f = std::get<factory_ptr< Base< T0, T1, T2 > >>(table); 
    return f(state.get(), param); 
    } 

    magic_factory(magic_factory&&)=default; 
    template<class T, 
    class=std::enable_if_t<!std::is_same<std::decay_t<T>, magic_factory>::value> 
    > 
    magic_factory(T&& t) { 
    ptr = {new std::decay_t<T> >(std::forward<T>(t)), 
     [](void* ptr){ 
     delete static_cast< std::decay_t<T>* >(ptr); 
     } 
    }; 
    // 64 lines like this: 
    std::get<factory_ptr< Base< std::int8_t, std::int8_t, std::int8_t > >>(table) 
    = +[](void* pvoid, tag_t<Base< std::int8_t, std::int8_t, std::int8_t >> tag, char const* param)->std::unique_ptr<Base< std::int8_t, std::int8_t, std::int8_t >> 
    { 
     auto*pt = static_cast<std::decay_t<T>*>(pvoid); 
     return (pt)(tag, param); 
    }; 
    } 
}; 

où nous construisons une tuple de fonctions de type effacement et un pointeur vide à son argument et nous répartir nous-mêmes.

Vous pouvez également utiliser certaines des machines ci-dessus pour automatiser cela. La table manuelle donne de l'efficacité, car nous ne dupliquons pas l'état de l'objet appelable N fois comme avec la version std::function. Une autre approche consiste à utiliser une solution my type erasing type erasure.

Nous écrivons un ensemble de modèles any_method s qui font le tour de tag pour créer l'objet Base<A,B,C>. Nous créons ensuite un super_any sur chacun des any_method s.

Puis nous héritons de cela et enveloppons votre make pour expédier à ceux any_method s.

Cela pourrait être à peu près aussi efficace que l'approche manuelle juste au-dessus.

template<class T0, class T1, class T2> 
auto make_base = make_any_method<std::unique_ptr<Base<T0, T1, T2>>(char const* name)>(
    [](auto* p, char const* name) 
    { 
    return p->make<T0, T1, T2>(name); 
    } 
); 
template<class T0, class T1, class T2> 
using base_maker = decltype(make_base); 

Maintenant, notre usine est:

super_any< base_maker<std::int8_t, std::int8_t, std::int8_t > > bob; 

bob peut stocker un pointeur vers une classe qui implémente make<T0, T1, T2> où ils correspondent int8_t, int8_t, int8_t.

Ajouter 64 lignes supplémentaires et terminé.

Les types bob magasins n'ont pas besoin d'avoir une classe de base commune du tout. Il n'utilise pas l'héritage C++ pour implémenter le polymorphisme, mais plutôt l'effacement manuel.

Pour ce que vous appelez simplement

auto r = (bob->*make_base<std::int8_t, std::int8_t, std::int8_t>)("hello"); 

passer naturellement plus de types super_any et il suppports plusieurs types de make_base.