Фабричный метод для шаблонных классов - PullRequest
0 голосов
/ 12 ноября 2018

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

Что это пытается решить:

Значения id() отправляются по сети, как только соединение установлено, и указывают получателю, как кодируется последовательность байтов. Приемник заранее знает тип T, который он ожидает, но не знает, как этот тип T кодируется на проводе, пока не получит это значение. Он также определяет, как возвращаемые значения (некоторого типа U, где U может иметь или не быть того же типа, что и T) должны быть упорядочены при возвращении. Этот код используется обычно, то есть есть несколько отправителей / получателей, которые используют / ожидают разные типы; однако типы, используемые между данной парой отправитель / получатель, всегда фиксированы.

Базовый эскиз проблемы: у нас есть (упрощенный) базовый класс, который определяет id()

template <typename T>
class foo
{
public:

    virtual ~foo() { }

    // Other methods

    // This must return the same value for every type T
    virtual std::uint8_t id() const noexcept = 0;
};

Оттуда у нас есть несколько подклассов:

template <typename T>
class bar : public foo<T>
{
public:
    std::uint8_t id() const noexcept override { return 1; }
};

template <typename T>
class quux : public foo<T>
{
public:
    std::uint8_t id() const noexcept override { return 2; }
};

Для фактической заводской функции мне нужно хранить что-то, что стирает тип (например, bar, quux), чтобы я мог сохранить фактический функция создания в однородном контейнере. По сути, я хочу семантику, которая примерно эквивалентна:

struct creation_holder
{
    // Obviously this cannot work, as we cannot have virtual template functions
    template <typename T>
    virtual foo<T>* build() const;
};

template <template <typename> class F>
struct create : public creation_holder
{
    // As above
    template <typename T>
    foo<T>* build() const override
    {
        return new F<T>();
    }
};

std::unordered_map<std::uint8_t, create*>& mapping()
{
    static std::unordered_map<std::uint8_t, create*> m;
    return m;
}

template <typename T, template <typename> class F>
bool register_foo(F<T> foo_subclass,
    typename std::enable_if<std::is_base_of<foo<T>, F<T>>::value>::type* = 0)
{
    auto& m = mapping();
    const auto id = foo_subclass.id();
    creation_holder* hold = new create<F>();
    // insert into map if it's not already present
}

template <typename T>
foo<T>* from_id(std::uint8_t id)
{
    const auto& m = mapping();
    auto it = m.find(id);
    if(it == m.end()) { return nullptr; }
    auto c = it->second;
    return c->build<T>();
}

Я поиграл с несколькими идеями, чтобы попытаться получить что-то подобное семантика, но без удачи. Есть ли способ сделать это (мне все равно, если реализация существенно отличается).

1 Ответ

0 голосов
/ 12 ноября 2018

Некоторые типы утилит для передачи типов и групп типов:

template<class...Ts>
struct types_t {};
template<class...Ts>
constexpr types_t<Ts...> types{}; // C++14.  In C++11, replace types<T> with types_t<T>{}.  Then again, I don't use it.
template<class T>
struct tag_t {};
template<class T>
constexpr tag_t<T> tag{}; // C++14.  In C++11, replace tag<T> with tag_t<T>{} below

Теперь мы пишем полифабрику.

Вот это ifactory:

template<template<class...>class Z, class T>
struct ifactory {
  virtual std::unique_ptr<Z<T>> tagged_build(tag_t<T>) const = 0;
  virtual ~ifactory() {}
};

Вы передаете тег, который хотите построить, и получаете объект. Довольно просто.

Затем мы объединяем их (это было бы проще в 1 , но вы запросили ):

template<template<class...>class Z, class Types>
struct poly_ifactory_impl;

Корпус одного типа:

template<template<class...>class Z, class T>
struct poly_ifactory_impl<Z,types_t<T>>:
  ifactory<Z, T>
{
  using ifactory<Z, T>::tagged_build;
};

корпус 2+:

template<template<class...>class Z, class T0, class T1, class...Ts>
struct poly_ifactory_impl<Z,types_t<T0, T1, Ts...>>:
  ifactory<Z, T0>,
  poly_ifactory_impl<Z, types_t<T1, Ts...>>
{
  using ifactory<Z, T0>::tagged_build;
  using poly_ifactory_impl<Z, types_t<T1, Ts...>>::tagged_build;
};

Мы импортируем метод tagged_build в производные классы. Это означает, что наиболее производный poly_ifactory_impl имеет все методы tagged_build в одном наборе перегрузки. Мы будем использовать это для отправки к ним.

Тогда мы завернем это довольно:

template<template<class...>class Z, class Types>
struct poly_ifactory:
  poly_ifactory_impl<Z, Types>
{
  template<class T>
  std::unique_ptr<Z<T>> build() const {
    return this->tagged_build(tag<T>);
  }
};

обратите внимание, я возвращаю unique_ptr; извлечение необработанного T* из фабричного метода - это кодовый запах.

Кто-то с poly_ifactory<?> просто делает ->build<T>() и игнорирует перегрузки tagged_ (если они не хотят их; я оставляю их открытыми). Каждый tagged_build является виртуальным, а build<T> - нет. Вот так мы эмулируем функцию виртуального шаблона.


Это обрабатывает интерфейс. С другой стороны, мы не хотим внедрять каждый build(tag_t<T>) вручную. Мы можем решить это с CRTP.

template<class D, class Base, template<class...>class Z, class T>
struct factory_impl : Base {
  virtual std::unique_ptr<Z<T>> tagged_build( tag_t<T> ) const override final {
    return static_cast<D const*>(this)->build_impl( tag<T> );
  }
  using Base::build;
};

template<class D, class Base, template<class...>class Z, class Types>
struct poly_factory_impl;

корпус 1 типа:

template<class D, class Base, template<class...>class Z, class T0>
struct poly_factory_impl<D, Base, Z, types_t<T0>> :
  factory_impl<D, Base, Z, T0>
{
  using factory_impl<D, Base, Z, T0>::tagged_build;
};

корпус типа 2+:

template<class D, class Base, template<class...>class Z, class T0, class T1, class...Ts>
struct poly_factory_impl<D, Base, Z, types_t<T0, T1, Ts...>> :
  factory_impl<D, poly_factory_impl<D, Base, Z, types_t<T1, Ts...>>, Z, T0>
{
  using factory_impl<D, poly_factory_impl<D, Base, Z, types_t<T1, Ts...>>, Z, T0>::tagged_build;
};

, что это делает, пишет серию tagged_build(tag_t<T>) перегрузок ifactory методов и перенаправляет их на D::build_impl(tag_t<T>), где D - теоретически производный тип.

Причудливая "обходная база" существует, чтобы избежать необходимости использовать виртуальное наследование. Мы наследуем линейно, каждый шаг реализуя одну tagged_build(tag<T>) перегрузку. Все они отправляют вниз не виртуально, используя CRTP.

Использование выглядит так:

struct bar {};

using my_types = types_t<int, double, bar>;

template<class T>
using vec = std::vector<T>;
using my_ifactory = poly_ifactory< vec, my_types >;

struct my_factory :
  poly_factory_impl< my_factory, my_ifactory, vec, my_types >
{
  template<class T>
  std::unique_ptr< vec<T> > build_impl( tag_t<T> ) const {
    return std::make_unique< std::vector<T> >( sizeof(T) );
    // above is C++14; in C++11, use:
    // return std::unique_ptr<vec<T>>( new vec<T>(sizeof(T)) );
  }
};

и экземпляр my_factory удовлетворяет интерфейсу my_ifactory.

В этом случае мы создаем уникальный ptr для вектора T с количеством элементов, равным sizeof(T). Это просто игрушка.

Живой пример .


Дизайн псевдокода.

Интерфейс имеет

template<class T> R build

функция. Отправляется на

virtual R tagged_build(tag_t<T>) = 0;

методы.

Указанные T извлечены из списка types_t<Ts...>. Поддерживаются только эти типы.

На стороне реализации мы создаем линейное наследование помощников CRTP. Каждый наследует от последнего и переопределяет virtual R tagged_build(tag_t<T>).

Реализация tagged_build использует CRTP для приведения указателя this к более производному классу и вызова build_impl(tag<T>) для него. Это пример полиморфизма без времени выполнения.

Итак, звонки идут с build<T> на virtual tagged_build(tag_t<T>) на build_impl(tag<T>). Пользователи просто взаимодействуют с одним шаблоном; Разработчики просто реализуют один шаблон. Клей в середине - virtual tagged_build - создается из списка types_t типов.

Это около 100 строк «связующего» или вспомогательного кода, и взамен мы получаем эффективные методы виртуальных шаблонов.


1 в это становится:

template<template<class...>class Z, class...Ts>
struct poly_ifactory_impl<Z,types_t<Ts...>>:
  ifactory<Z, Ts>...
{
  using ifactory<Z, Ts>::tagged_build...;
};

что намного проще и понятнее.


Наконец, вы можете сделать что-то неопределенно , как это, без центрального списка типов. Если вы знаете, как вызывающий, так и вызываемый знают тип, который вы могли бы передать typeid или typeindex в ifactory, передать void* или что-то подобное через механизм виртуальной диспетчеризации и привести / проверить на ноль / сделать поиск в карте для типов.

Внутренняя реализация будет выглядеть аналогично этой, но вам не придется публиковать types_t как часть вашего формального (или двоичного) интерфейса.

Внешне вам придется «просто знать», какие типы поддерживаются. Во время выполнения вы можете получить нулевой умный (или тупой, ick) указатель, если вы передадите неподдерживаемый тип.

С некоторой осторожностью вы могли бы сделать и то и другое. Предоставьте эффективный и безопасный механизм для получения известных типов во время компиляции, примененных к шаблону. Также предоставьте интерфейс на основе «try», который использует эффективную известную во время компиляции систему (если тип соответствует) и использует неэффективную проверенную среду выполнения. Это можно сделать по эзотерическим причинам обратной двоичной совместимости (поэтому новое программное обеспечение может подключаться через устаревший интерфейс к новым или старым реализациям API и динамически обрабатывать наличие старой реализации API).

Но в этот момент вы рассматривали возможность использования COM?

...