web-dev-qa-db-fra.com

Remplacement conditionnel dans un modèle de classe dérivée

J'ai une classe Container qui contient des objets dont le type peut être dérivé de n'importe quelle combinaison de certaines classes de base (TypeA, TypeB, etc.). La classe de base de Container a des méthodes virtuelles qui renvoient un pointeur sur l'objet contenu; ceux-ci doivent renvoyer nullptr si l'objet contenu n'est pas dérivé de la classe attendue. Je voudrais remplacer de manière sélective les méthodes de la base en fonction du paramètre de modèle de Container. J'ai essayé d'utiliser SFINAE comme suit, mais il ne compile pas. Je voudrais éviter de spécialiser Container pour chaque combinaison possible car il pourrait y en avoir beaucoup.

#include <type_traits>
#include <iostream>

using namespace std;

class TypeA {};
class TypeB {};
class TypeAB: public TypeA, public TypeB {};

struct Container_base {
    virtual TypeA* get_TypeA() {return nullptr;}
    virtual TypeB* get_TypeB() {return nullptr;}
};

template <typename T>
struct Container: public Container_base
{
    Container(): ptr(new T()) {}

    //Override only if T is derived from TypeA
    auto get_TypeA() -> enable_if<is_base_of<TypeA, T>::value, TypeA*>::type
    {return ptr;}

    //Override only if T is dervied from TypeB
    auto get_TypeB() -> enable_if<is_base_of<TypeB, T>::value, TypeB*>::type
    {return ptr;}

private:
    T* ptr;
};

int main(int argc, char *argv[])
{
    Container<TypeA> typea;
    Container<TypeB> typeb;
    Container<TypeAB> typeab;

    cout << typea.get_TypeA() << endl; //valid pointer
    cout << typea.get_TypeB() << endl; //nullptr

    cout << typeb.get_TypeA() << endl; //nullptr
    cout << typeb.get_TypeB() << endl; //valid pointer

    cout << typeab.get_TypeA() << endl; //valid pointer
    cout << typeab.get_TypeB() << endl; //valid pointer

    return 0;
}
9
Carlton

... ou vous pouvez changer votre approche en une approche plus simple:

template <typename T>
struct Container: public Container_base
{
    TypeA* get_TypeA() override
    {
        if constexpr(is_base_of_v<TypeA, T>)
            return ptr;
        else
            return nullptr;
    }

    ...
};

et comptez sur l'optimiseur pour lisser les rides. C'est comme remplacer plusieurs fonctions return nullptr par une (en binaire final). Ou supprimer la branche morte du code si votre compilateur ne prend pas en charge if constexpr.

Modifier:

... ou (si vous insistez pour utiliser SFINAE) quelque chose dans ce sens:

template<class B, class T, enable_if_t< is_base_of_v<B, T>>...> B* cast_impl(T* p) { return p; }
template<class B, class T, enable_if_t<!is_base_of_v<B, T>>...> B* cast_impl(T* p) { return nullptr; }

template <typename T>
struct Container: public Container_base
{
    ...

    TypeA* get_TypeA() override { return cast_impl<TypeA>(ptr); }
    TypeB* get_TypeB() override { return cast_impl<TypeB>(ptr); }

private:
    T* ptr;
};
3
C.M.

Le CRTP à la rescousse!

template<class T, class D, class Base, class=void>
struct Container_getA:Base {};
template<class T, class D, class Base, class=void>
struct Container_getB:Base {};

template<class T, class D, class Base>
struct Container_getA<T, D, Base, std::enable_if_t<std::is_base_of<TypeA,T>{}>>:
  Base
{
  TypeA* get_TypeA() final { return self()->ptr; }
  D* self() { return static_cast<D*>(this); }
};

template<class T, class D, class Base>
struct Container_getB<T, D, Base, std::enable_if_t<std::is_base_of<TypeB,T>{}>>:
  Base
{
  TypeB* get_TypeB() final { return self()->ptr; }
  D* self() { return static_cast<D*>(this); }
};

template <class T>
struct Container: 
  Container_getA< T, Container<T>,
    Container_getB< T, Container<T>,
      Container_base
    >
  >
{
    Container(): ptr(new T()) {}

public: // either public, or complex friend declarations; just make it public
    T* ptr;
};

et fait.

Vous pouvez faire un peu de travail pour permettre:

struct Container: Bases< T, Container<T>, Container_getA, Container_getB, Container_getC >

ou similaire où nous plions les bases CRTP en.

Vous pouvez également nettoyer votre syntaxe:

template<class...Ts>
struct types {};

template<class T>
struct tag_t {using type=T;};
template<class T>
constexpr tag_t<T> tag{};

Ensuite, au lieu d’avoir une pile de getters nommés, ayez:

template<class List>
struct Container_getters;

template<class T>
struct Container_get {
  virtual T* get( tag_t<T> ) { return nullptr; }
};
template<class...Ts>
struct Container_getters<types<Ts...>>:
  Container_get<Ts>...
{
   using Container_get<Ts>::get...; // C++17
   template<class T>
   T* get() { return get(tag<T>); }
};

et maintenant une liste de types centrale peut être utilisée pour gérer l'ensemble des types que vous pouvez obtenir à partir du conteneur.

Nous pouvons ensuite utiliser cette liste de types centrale pour écrire les aides intermédiaires CRTP.

template<class Actual, class Derived, class Target, class Base, class=void>
struct Container_impl_get:Base {};
template<class Actual, class Derived, class Target, class Base>
struct Container_impl_get<Actual, Derived, Target, Base,
  std::enable_if_t<std::is_base_of<Target, Actual>{}>
>:Base {
  using Base::get;
  virtual Target* get( tag_t<Target> ) final { return self()->ptr; }
  Derived* self() { return static_cast<Derived*>(this); }
};

et maintenant nous avons juste besoin d'écrire les machines de pliage.

template<class Actual, class Derived, class List>
struct Container_get_folder;
template<class Actual, class Derived, class List>
using Container_get_folder_t=typename Container_get_folder<Actual, Derived, List>::type;

template<class Actual, class Derived>
struct Container_get_folder<Actual, Derived, types<>> {
  using type=Container_base;
};
template<class Actual, class Derived, class T0, class...Ts>
struct Container_get_folder<Actual, Derived, types<T0, Ts...>> {
  using type=Container_impl_get<Actual, Derived, T0,
    Container_get_folder_t<Actual, Derived, types<Ts...>>
  >;
};

alors nous obtenons

using Container_types = types<TypeA, TypeB, TypeC>;
struct Container_base:Container_getters<Container_types> {
};

template <typename T>
struct Container: Container_get_folder_t<T, Container<T>, Container_types>
{
    Container(): ptr(new T()) {}
    T* ptr;
};

et maintenant nous pouvons étendre cela en ajoutant simplement un type à Container_types.

Les appelants qui souhaitent un type spécifique peuvent soit:

Container_base* ptr = /* whatever */;
ptr->get<TypeA>()

ou

ptr->get(tag<TypeA>);

les deux fonctionnent également bien.

Exemple en direct - il utilise une fonctionnalité C++ 14 ou deux (à savoir des modèles de variable dans tag), mais vous pouvez remplacer tag<X> par tag_t<X>{}.

3

J'ai essayé d'utiliser SFINAE comme suit, mais il ne compile pas. Je voudrais éviter de spécialiser le conteneur pour chaque combinaison possible car il pourrait y en avoir beaucoup.

Malheureusement, les fonctions virtuelles et les fonctions de modèle sont incompatibles. Et vous ne pouvez pas utiliser SFINAE avec des méthodes non template, donc 

auto get_TypeA()
   -> typename std::enable_if<std::is_base_of<TypeA, T>::value, TypeA*>::type
 {return ptr;}

ne fonctionne pas car le type T est un argument de modèle de la classe, pas un argument de modèle de la méthode.

Pour activer SFINAE, vous pouvez gabarit la méthode comme suit

template <typename U = T>
auto get_TypeA()
   -> typename std::enable_if<std::is_base_of<TypeA, U>::value, TypeA*>::type
 {return ptr;}

et maintenant SFINAE fonctionne, mais get_TypeA() est une méthode de modèle et ne peut donc plus être virtuelle.

Si vous avez vraiment besoin de fonctions virtuelles, vous pouvez résoudre le problème avec la spécialisation héritage et modèles (voir la réponse de Yakk).

Mais, si vous n'avez pas vraiment besoin que les fonctions get_TypeX() soient virtuelles, je vous propose une solution complètement différente (et plus simple, je suppose), entièrement basée sur un couple (quel que soit le nombre de classes TypeX) de méthodes template.

Je veux dire ... si vous écrivez quelques méthodes de modèle get_Type() alternatives comme suit

  template <typename U>
  auto get_Type()
     -> std::enable_if_t<true == std::is_base_of<U, T>::value, U*>
   { return ptr; }

  template <typename U>
  auto get_Type()
     -> std::enable_if_t<false == std::is_base_of<U, T>::value, U*>
   { return nullptr; }

vous n'avez plus besoin de Container_base et le type du pointeur demandé devient le paramètre de modèle de la méthode appelée comme suit

typea.get_Type<TypeA>()

Voici un exemple complet de C++ 14 (si vous avez besoin d’une solution C++ 11, utilisez simplement typename std::enable_if<>::type au lieu de std::enable_if_t<>).

#include <type_traits>
#include <iostream>

class TypeA {};
class TypeB {};
class TypeAB: public TypeA, public TypeB {};

template <typename T>
struct Container
 {
   private:
      T* ptr;

   public:
      Container(): ptr{new T{}} {}

      template <typename U>
      auto get_Type()
         -> std::enable_if_t<true == std::is_base_of<U, T>::value, U*>
       { return ptr; }

      template <typename U>
      auto get_Type()
         -> std::enable_if_t<false == std::is_base_of<U, T>::value, U*>
       { return nullptr; }
};

int main ()
 {
   Container<TypeA> typea;
   Container<TypeB> typeb;
   Container<TypeAB> typeab;

   std::cout << typea.get_Type<TypeA>() << std::endl; //valid pointer
   std::cout << typea.get_Type<TypeB>() << std::endl; //nullptr

   std::cout << typeb.get_Type<TypeA>() << std::endl; //nullptr
   std::cout << typeb.get_Type<TypeB>() << std::endl; //valid pointer

   std::cout << typeab.get_Type<TypeA>() << std::endl; //valid pointer
   std::cout << typeab.get_Type<TypeB>() << std::endl; //valid pointer
 }
2
max66