Общий фабричный механизм в C ++ 17 - PullRequest
3 голосов
/ 07 марта 2019

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

В идеале решение должно использовать только функции C ++ 17 (без зависимостей).

Рассмотрим этот пример

#include <iostream>
#include <string>
#include <memory>

struct Foo {
    virtual ~Foo() = default;
    virtual void hello() = 0;
};

struct FooA: Foo { 
    static constexpr char const* name = "A";
    void hello() override { std::cout << "Hello " << name << std::endl; }
};

struct FooB: Foo { 
    static constexpr char const* name = "B";
    void hello() override { std::cout << "Hello " << name << std::endl; }
};

struct FooC: Foo { 
    static constexpr char const* name = "C";
    void hello() override { std::cout << "Hello " << name << std::endl; }
};

struct BarInterface {
    virtual ~BarInterface() = default;
    virtual void world() = 0;
};

template <class T>
struct Bar: BarInterface {
    void world() { std::cout << "World " << T::name << std::endl; }
};

std::unique_ptr<Foo> foo_factory(const std::string& name) {
    if (name == FooA::name) {
        return std::make_unique<FooA>();
    } else if (name == FooB::name) {
        return std::make_unique<FooB>();
    } else if (name == FooC::name) {
        return std::make_unique<FooC>();
    } else {
        return {};
    }
}

std::unique_ptr<BarInterface> bar_factory(const std::string& foo_name) {
    if (foo_name == FooA::name) {
        return std::make_unique<Bar<FooA>>();
    } else if (foo_name == FooB::name) {
        return std::make_unique<Bar<FooB>>();
    } else if (foo_name == FooC::name) {
        return std::make_unique<Bar<FooC>>();
    } else {
        return {};
    }
}

int main()
{
    auto foo = foo_factory("A");
    foo->hello();
    auto bar = bar_factory("C");
    bar->world();
}

запустить его

Я ищу механизм, который позволил бы мне реализовать foo_factory и bar_factory без перечисления всех классов, так что их не нужно обновлять после добавления, например, FooD в качестве дополнительного производного класса , В идеале, различные производные Foo должны были бы как-то «самостоятельно регистрироваться», но перечисление их всех в одном центральном месте также приемлемо.

Edit:

Некоторые пояснения, основанные на комментариях / ответах:

  • В моем случае необходимо вызывать фабрики с (что-то вроде) строкой, так как вызывающие фабрики используют полиморфизм с Foo / BarInterface, то есть они не знают о конкретных производных классах. С другой стороны, в Bar мы хотим использовать шаблонные методы производных классов Foo и облегчить встраивание, поэтому нам действительно нужны шаблонные производные классы Bar (а не доступ к объектам Foo через некоторый интерфейс базового класса).
  • Мы можем предположить, что все производные классы Foo определены в одном месте (и поэтому при необходимости допустима ручная регистрация, где мы перечисляем их все один раз в одном и том же месте). Однако они не знают о существовании Bar, и на самом деле у нас есть несколько разных классов, таких как BarInterface и Bar. Поэтому мы не можем создавать «объекты-конструкторы» Bar и сохранять их на карте так же, как мы это делаем для foo_factory. Я думаю, что нужна какая-то «карта времени компиляции» (или список) всех производных типов Foo, так что при определении bar_factory компилятор может их перебирать, но я не знаю, как это сделать ...

Edit2:

Дополнительные ограничения, которые подтвердили свою актуальность во время обсуждения :

  • Шаблоны и шаблоны шаблонов: Foo - это фактически шаблоны (с одним аргументом класса), а Bar - шаблоны шаблонов, принимающие конкретный Foo в качестве аргумента шаблона. Шаблоны Foo не имеют специализаций и имеют одинаковое «имя», поэтому запросы к любому конкретному типу вполне подходят. В частности SpecificFoo<double>::name всегда действует. Ответ @Julius был расширен, чтобы облегчить это уже. Для @ Yakk то же самое, вероятно, можно сделать (но мне потребуется некоторое время, чтобы разобраться в этом подробнее).
  • Заводской код гибкой панели: Фабрика для Bar делает чуть больше, чем просто вызывает конструктор. Он также передает некоторые аргументы и выполняет приведение типов (в частности, он может иметь ссылки на Foo, которые должны быть dynamic_cast на соответствующий конкретный производный Foo). Поэтому решение, которое позволяет писать этот код в процессе определения bar_factory, кажется мне наиболее читаемым. Ответ @Julius здесь прекрасно работает, даже если код цикла с кортежами немного многословен.
  • Упрощение "единого места", перечисляющего Foos: Из ответов, которые я до сих пор считаю, я считаю, что путь для меня - это список типов foo во время компиляции и способ перебораих.Есть два ответа, которые определяют список типов (или шаблонов) Foo в одном центральном месте (либо с шаблоном types, либо с кортежами), что уже здорово.Однако по другим причинам у меня уже есть в том же центральном месте список вызовов макросов, по одному для каждого foo, например DECLARE_FOO(FooA, "A") DECLARE_FOO(FooB, "B") ....Может ли декларация FooTypes каким-то образом воспользоваться этим, поэтому мне не нужно перечислять их снова?Я думаю, что такие списки типов не могут быть объявлены итеративно (добавление к уже существующему списку), или это возможно?В отсутствие этого, возможно, с некоторой макромагой это было бы возможно.Может быть, всегда переопределять и, таким образом, добавлять в список препроцессоров в вызовах DECLARE_FOO, а затем, наконец, выполнить «итерацию по циклу», чтобы определить список типов FooTypes.В препроцессоре повышения IIRC есть средства для циклического перебора списков (хотя я не хочу повышать зависимость).

Еще немного context, вы можете рассматривать различные Foo и его аргументы шаблона как классыаналогично Eigen::Matrix<Scalar> и Bar - это функторы стоимости, которые будут использоваться с Ceres.Фабрика баров возвращает объекты типа ceres::AutoDiffCostFunction<CostFunctor<SpecificFoo>, ...> в виде ceres::CostFunction* указателей.

Edit3:

На основе ответа @Julius я создал решение, которое работает с Bars,являются шаблонами, а также шаблоны шаблонов.Я подозреваю, что можно объединить bar_tmpl_factory и bar_ttmpl_factory в одну функцию, используя шаблоны шаблонов переменных переменных (это так?).

запустить его

TODO:

  • объединить bar_tmpl_factory и bar_ttmpl_factory
  • точка Making the "single place" listing the Foos even simpler отвыше
  • возможно, заменив использование кортежей шаблоном @ Yakk's types (но таким образом, что функция создателя может быть определена inline на сайте вызова цикла по всем fooтипы).

Я считаю, что на вопрос дан ответ, и, если что-то и выше, пункты должны быть отдельными вопросами.

Ответы [ 4 ]

3 голосов
/ 08 марта 2019
template<class...Ts>struct types_t {};
template<class...Ts>constexpr types_t<Ts...> types{};

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

template<class T>
struct tag_t { using type=T;
  template<class...Ts>
  constexpr decltype(auto) operator()(Ts&&...ts)const {
    return T{}(std::forward<Ts>(ts)...);
  }
};
template<class T>
constexpr tag_t<T> tag{};

это позволяет нам работать с типами в качестве значений.

Теперь карта тегов типа - это функция, которая принимает тег типа и возвращает другой тег типа.

template<template<class...>class Z>
struct template_tag_map {
  template<class In>
  constexpr decltype(auto) operator()(In in_tag)const{
    return tag< Z< typename decltype(in_tag)::type > >;
  }
};

это берет карту типа шаблона и превращает ее в карту тегов.

template<class R=void, class Test, class Op, class T0 >
R type_switch( Test&&, Op&& op, T0&&t0 ) {
  return static_cast<R>(op(std::forward<T0>(t0)));
}

template<class R=void, class Test, class Op, class T0, class...Ts >
auto type_switch( Test&& test, Op&& op, T0&& t0, Ts&&...ts )
{
  if (test(t0)) return static_cast<R>(op(std::forward<T0>(t0)));
  return type_switch<R>( test, op, std::forward<Ts>(ts)... );
}

, который позволяет нам тестировать условие для группы типов и запускать операцию для той, которая "успешно".

template<class R, class maker_map, class types>
struct named_factory_t;

template<class R, class maker_map, class...Ts>
struct named_factory_t<R, maker_map, types_t<Ts...>>
{
  template<class... Args>
  auto operator()( std::string_view sv, Args&&... args ) const {
    return type_switch<R>(
      [&sv](auto tag) { return decltype(tag)::type::name == sv; },
      [&](auto tag) { return maker_map{}(tag)(std::forward<Args>(args)...); },
      tag<Ts>...
    );
  }
};

Теперь мы хотим создать общие указатели для некоторого шаблона.

struct shared_ptr_maker {
  template<class Tag>
  constexpr auto operator()(Tag ttag) {
    using T=typename decltype(ttag)::type;
    return [](auto&&...args){ return std::make_shared<T>(decltype(args)(args)...); };
  }
};

так что общие указатели получают тип.

template<class Second, class First>
struct compose {
  template<class...Args>
  constexpr decltype(auto) operator()(Args&&...args) const {
    return Second{}(First{}( std::forward<Args>(args)... ));
  }
};

теперь мы можем составлять функциональные объекты во время компиляции.

Далее подключите.

using Foos = types_t<FooA, FooB, FooC>;
constexpr named_factory_t<std::shared_ptr<Foo>, shared_ptr_maker, Foos> make_foos;

constexpr named_factory_t<std::shared_ptr<BarInterface>, compose< shared_ptr_maker, template_tag_map<Bar> >, Foos> make_bars;

и Готово .

Оригинальный дизайн был на самом деле с лямбдами вместо этих struct s для shared_ptr_maker и т. П.

И make_foos, и make_bars имеют нулевое состояние времени выполнения.

2 голосов
/ 07 марта 2019

Напишите общую фабрику, например, такую, которая позволяет регистрироваться на сайте класса:

template <typename Base>
class Factory {
public:
    template <typename T>
    static bool Register(const char * name) {
       get_mapping()[name] = [] { return std::make_unique<T>(); };
       return true;
    }
    static std::unique_ptr<Base> factory(const std::string & name) {
        auto it = get_mapping().find(name);
        if (it == get_mapping().end())
            return {};
        else
            return it->second();
    }

private:
    static std::map<std::string, std::function<std::unique_ptr<Base>()>> & get_mapping() {
        static std::map<std::string, std::function<std::unique_ptr<Base>()>> mapping;
        return mapping;
    }
};

А затем используйте его как:

struct FooA: Foo {
    static constexpr char const* name = "A";
    inline static const bool is_registered = Factory<Foo>::Register<FooA>(name);
    inline static const bool is_registered_bar = Factory<BarInterface>::Register<Bar<FooA>>(name);
    void hello() override { std::cout << "Hello " << name << std::endl; }
};

и

std::unique_ptr<Foo> foo_factory(const std::string& name) {
    return Factory<Foo>::factory(name);
}

Примечание: нет способа гарантировать, что класс будет зарегистрирован. Компилятор может решить не включать модуль перевода, если нет других зависимостей. Вероятно, лучше просто зарегистрировать все классы в одном центральном месте. Также обратите внимание, что саморегистрационная реализация зависит от встроенных переменных (C ++ 17). Это не сильная зависимость, и от нее можно избавиться, объявив логические значения в заголовке и определив их в CPP (что делает саморегистрацию более уродливой и более склонной к невозможности регистрации).

редактировать

  1. Недостатком этого ответа по сравнению с другими является то, что он выполняет регистрацию во время запуска, а не во время компиляции. С другой стороны, это делает код намного проще.
  2. В приведенных выше примерах предполагается, что определение Bar<T> перенесено выше Foo. Если это невозможно, тогда регистрация может быть выполнена в функции инициализации, в cpp:

    // If possible, put at the header file and uncomment:
    // inline
    const bool barInterfaceInitialized = [] {
       Factory<Foo>::Register<FooA>(FooA::name);
       Factory<Foo>::Register<FooB>(FooB::name);
       Factory<Foo>::Register<FooC>(FooC::name);
       Factory<BarInterface>::Register<Bar<FooA>>(FooA::name);
       Factory<BarInterface>::Register<Bar<FooB>>(FooB::name);
       Factory<BarInterface>::Register<Bar<FooC>>(FooC::name);
       return true;
    }();
    
1 голос
/ 08 марта 2019

Мне кажется, что нужна какая-то «карта времени компиляции» (или список) всех производных типов Foo, так что при определении bar_factory компилятор может их перебирать, но я нене знаю, как это сделать ...

Вот один из основных вариантов:

#include <cassert>

#include <tuple>
#include <utility>

#include "foo_and_bar_without_factories.hpp"

////////////////////////////////////////////////////////////////////////////////

template<std::size_t... indices, class LoopBody>
void loop_impl(std::index_sequence<indices...>, LoopBody&& loop_body) {
  (loop_body(std::integral_constant<std::size_t, indices>{}), ...);
}

template<std::size_t N, class LoopBody>
void loop(LoopBody&& loop_body) {
  loop_impl(std::make_index_sequence<N>{}, std::forward<LoopBody>(loop_body));
}

////////////////////////////////////////////////////////////////////////////////

using FooTypes = std::tuple<FooA, FooB, FooC>;// single registration

std::unique_ptr<Foo> foo_factory(const std::string& name) {
  std::unique_ptr<Foo> ret{};

  constexpr std::size_t foo_count = std::tuple_size<FooTypes>{};

  loop<foo_count>([&] (auto i) {// `i` is an std::integral_constant
    using SpecificFoo = std::tuple_element_t<i, FooTypes>;
    if(name == SpecificFoo::name) {
      assert(!ret && "TODO: check for unique names at compile time?");
      ret = std::make_unique<SpecificFoo>();
    }
  });

  return ret;
}

std::unique_ptr<BarInterface> bar_factory(const std::string& name) {
  std::unique_ptr<BarInterface> ret{};

  constexpr std::size_t foo_count = std::tuple_size<FooTypes>{};

  loop<foo_count>([&] (auto i) {// `i` is an std::integral_constant
    using SpecificFoo = std::tuple_element_t<i, FooTypes>;
    if(name == SpecificFoo::name) {
      assert(!ret && "TODO: check for unique names at compile time?");
      ret = std::make_unique< Bar<SpecificFoo> >();
    }
  });

  return ret;
}
1 голос
/ 07 марта 2019

В C ++ 17 мы можем применить выражение сгиба, чтобы упростить процесс сохранения генерации функций std::make_unique<FooA>(), std::make_unique<FooB>() и т. Д. В данном случае в фабричном классе.


Для начала давайте определим псевдоним типа Generator, который описывает тип каждой генерирующей функции [](){ return std::make_unique<T>(); }:

template<typename T>
using Generator = std::function<std::unique_ptr<T>(void)>;

Далее мы определим следующий довольно общий функтор createFactory, который возвращает каждую фабрику в виде хэш-карты std::unordered_map. Здесь я применяю выражение сгиба с помощью операторов запятой. Например, createFactory<BarInterface, Bar, std::tuple<FooA, FooB, FooC>>()() возвращает хеш-карту, соответствующую вашей функции bar_factory:

template<typename BaseI, template<typename> typename I, typename T>
void inserter(std::unordered_map<std::string_view, Generator<BaseI>>& map)
{
    map.emplace(T::name, [](){ return std::make_unique<I<T>>(); });
}

template<typename BaseI, template<typename> class I, typename T>
struct createFactory {};

template<typename BaseI, template<typename> class I, typename... Ts>
struct createFactory<BaseI, I, std::tuple<Ts...>>
{
    auto operator()()
    {
        std::unordered_map<std::string_view, Generator<BaseI>> map;
        (inserter<BaseI, I, Ts>(map), ...);

        return map;
    }
};

Этот функтор позволяет нам перечислить FooA, FooB, FooC, ... все в одном центральном месте следующим образом:

DEMO (где я добавил виртуальные деструкторы в базовые классы)

template<typename T>
using NonInterface = T;

// This can be written in one central place.
using FooTypes = std::tuple<FooA, FooB, FooC>;

int main()
{    
    const auto foo_factory = createFactory<Foo, NonInterface, FooTypes>()();
    const auto foo = foo_factory.find("A");
    if(foo != foo_factory.cend()){
        foo->second()->hello();
    }

    const auto bar_factory = createFactory<BarInterface, Bar, FooTypes>()();
    const auto bar = bar_factory.find("C");
    if(bar != bar_factory.cend()){
        bar->second()->world();
    }

    return 0;
}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...