Generic factory mechanism in C++17

和自甴很熟 提交于 2019-12-02 00:01:25

What I think is needed is some kind of "compile-time map" (or list) of all the derived Foo types, such that when defining the bar_factory, the compiler can iterate over them, but I don't know how to do that...

Here is one basic option:

#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;
}
Yakk - Adam Nevraumont
template<class...Ts>struct types_t {};
template<class...Ts>constexpr types_t<Ts...> types{};

that lets us work with bundles of types without the overhead of a tuple.

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

this lets us work with types as values.

Now a type tag map is a function that takes a type tag, and returns another type 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 > >;
  }
};

this takes a template type map and makes it into a tag map.

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

that lets us test a condition on a bunch of types, and run an operation on the one that "succeeds".

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

now we want to make shared pointers of some template class.

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

so that makes shared pointers given a type.

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

now we can compose function objects at compile time.

Next wire it up.

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;

and Done.

The original design was actually with lambdas instead of those structs for shared_ptr_maker and the like.

Both make_foos and make_bars have zero runtime state.

Write a generic factory like the following that allows registration at the class site:

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

And then use it like:

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

and

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

Note: there is no way to guarantee that the class would be registered. The compiler might decide not to include the translation unit, if there are no other dependencies. It is probably better to simply register all classes in one central place. Also note that the self-registering implementation depends on inline variables (C++17). It is not a strong dependence, and it is possible to get rid of it by declaring the booleans in the header and defining them in the CPP (which makes self-registering uglier and more prone to failing to register).

edit

  1. The disadvantage of this answer, when compared to others, is that it performs the registration during start-up and not during compilation. On the other hand, this makes the code much simpler.
  2. The examples above assume that the definition of Bar<T> is moved above Foo. If that is impossible, then the registration can be done in an initialization function, in a 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;
    }();
    

In C++17, we can apply the fold expression to simplify the storing process of generating functions std::make_unique<FooA>(), std::make_unique<FooB>(), and so on into the factory class in this case.


To begin with, for convenience, let us define the following type alias Generator which describes the type of each generating function [](){ return std::make_unique<T>(); }:

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

Next, we define the following rather generic functor createFactory which returns each factory as a hash map std::unordered_map. Here I apply the fold expression with the comma operators. For instance, createFactory<BarInterface, Bar, std::tuple<FooA, FooB, FooC>>()() returns the hash map corresponding to your function 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;
    }
};

This functor enables us to list FooA, FooB, FooC, ... all in one central place as follows:

DEMO (where I added virtusl destructors in base classes)

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;
}
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!