Условное переопределение в шаблоне производного класса

У меня есть класс Container, содержащий объекты, тип которых может быть получен из любой комбинации некоторых базовых классов (TypeA, TypeB и т. д.). Базовый класс Container имеет виртуальные методы, которые возвращают указатель на содержащийся объект; они должны возвращать nullptr, если содержащийся объект не является производным от ожидаемого класса. Я хотел бы выборочно переопределить методы базы на основе параметра шаблона Container. Я попытался использовать SFINAE следующим образом, но он не компилируется. Я хотел бы избежать специализации Container для каждой возможной комбинации, потому что их может быть много.

#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;
}

person Carlton    schedule 23.10.2018    source источник
comment
Для полноты картины причина, по которой ваш подход не работает, заключается в том, что геттеры производного класса скрывают геттеры базового класса на этапе поиска имени, даже несмотря на то, что позже они удаляются с помощью SFINAE, который происходит как часть вывода типа шаблона.   -  person Arne Vogel    schedule 25.10.2018


Ответы (3)


... или вы можете изменить свой подход на более простой:

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;
    }

    ...
};

и полагаться на оптимизатор, чтобы сгладить любые морщины. Например, заменить несколько функций return nullptr одной (в финальном двоичном коде). Или удалить мертвую ветку кода, если ваш компилятор не поддерживает if constexpr.

Изменить:

... или (если вы настаиваете на использовании SFINAE) что-то в этом роде:

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;
};
person C.M.    schedule 23.10.2018

CRTP спешит на помощь!

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;
};

и сделано.

Вы можете немного поработать, чтобы разрешить:

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

или тому подобное, где мы складываем базы CRTP.

Вы также можете очистить свой синтаксис:

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

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

Затем, вместо кучи именованных геттеров, есть:

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>); }
};

и теперь можно использовать центральный список типов для поддержки набора типов, которые вы можете получить из контейнера.

Затем мы можем использовать этот центральный список типов для написания промежуточных помощников 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); }
};

и теперь нам просто нужно написать механизм складывания.

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...>>
  >;
};

так что мы получаем

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;
};

и теперь мы можем расширить это, просто добавив тип к Container_types.

Вызывающие абоненты, которым нужен определенный тип, могут сделать следующее:

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

or

ptr->get(tag<TypeA>);

оба работают одинаково хорошо.

Живой пример -- он использует функцию C++14 или две (а именно переменную шаблоны в tag), но вы можете заменить tag<X> на tag_t<X>{}.

person Yakk - Adam Nevraumont    schedule 23.10.2018
comment
Вау, здесь есть что распаковать. Я протестировал первое решение, и оно работает (спасибо!), однако я не узнаю часть синтаксиса: что делают пустые фигурные скобки после std::is_base_of_t? Как в std::enable_if_t<std::is_base_of<TypeA,T>{}> - person Carlton; 23.10.2018
comment
@Carlton - std::is_base_of<TypeA,T>{} создать объект типа std::is_base_of<TypeA,T>. Объект типа std::is_base_of<TypeA,T> может быть преобразован в логический (возвращается std::is_base_of<TypeA,T>::value). Таким образом, std::is_base_of<TypeA,T>{} в контексте, требующем логического значения, является (существенно) более коротким способом записи std::is_base_of<TypeA,T>::value. - person max66; 23.10.2018
comment
@Carlton Я бы также рассмотрел std::is_convertible_t< Actual*, Target* >{}, который разрешает get<void>() или обрабатывает const хранилище или что-то еще. - person Yakk - Adam Nevraumont; 24.10.2018

Я попытался использовать SFINAE следующим образом, но он не компилируется. Я хотел бы избежать специализации Container для каждой возможной комбинации, потому что их может быть много.

К сожалению, виртуальные функции и функции шаблона несовместимы. И вы не можете использовать SFINAE с не шаблонными методами, поэтому

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

не работает, потому что тип T является аргументом шаблона класса, а не аргументом шаблона метода.

Чтобы включить SFINAE, вы можете шаблонизировать метод следующим образом.

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

и теперь SFINAE работает, но get_TypeA() теперь является шаблонным методом, поэтому больше не может быть виртуальным.

Если вам действительно нужны виртуальные функции, вы можете решить с помощью наследования и специализации шаблона (см. ответ Якка).

Но, если вам на самом деле не нужно, чтобы get_TypeX() функций были виртуальными, я предлагаю вам совершенно другое (и более простое, я полагаю) решение, полностью основанное на паре (независимо от количества TypeX классов) шаблонных методов.

Я имею в виду... если вы напишете пару альтернативных get_Type() методов шаблона следующим образом

  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; }

вам больше не нужен Container_base и тип запрошенного указателя становится параметром шаблона метода, который вызывается следующим образом

typea.get_Type<TypeA>()

Ниже приведен полный рабочий пример C++14 (если вам нужно решение C++11, просто используйте typename std::enable_if<>::type вместо 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
 }
person max66    schedule 23.10.2018
comment
На самом деле, я могу просто переопределить виртуальные функции моего базового класса с точки зрения ваших get_Type функций-членов шаблона; то есть TypeA* get_TypeA() override {return get_Type<TypeA>();}. Он производит правильное поведение. - person Carlton; 23.10.2018
comment
@Carlton - да ... если вам действительно нужно работать с виртуальной функцией для каждого типа TypeX ... - person max66; 23.10.2018