Generic factory mechanism in C++17

2020-04-11 03:24发布

I would like to implement a generic factory mechanism for a set of derived classes that allows me to generically implement not only a factory function to create objects of that class, but also creators of other template classes which take as template arguments one of the derived classes.

Ideally a solution would only use C++17 features (no dependencies).

Consider this example

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

run it

I am looking for a mechanism that would allow me to implement both foo_factory and bar_factory without listing all classes, such that they do not need to be updated once I add for example FooD as an additional derived class. Ideally, the different Foo derivatives would somehow "self-register", but listing them all in one central place is also acceptable.

Edit:

Some clarifications based on comments / answers:

  • It is necessary in my case to invoke the factories with (something like) a string, since the callers of the factories use polymorphism with Foo / BarInterface, i.e. they don't know about the concrete derived classes. On the other hand in Bar we want to use template methods of the derived Foo classes and facilitate inlining, that's why we really need the templated derived Bar classes (rather than accessing Foo objects through some base-class interface).
  • We can assume that all derived Foo classes are defined in one place (and a manual registration where we list them all once in the same place is therefore acceptable, if necessary). However, they do not know about the existence of Bar, and in fact we have multiple different classes like BarInterface and Bar. So we cannot create "constructor objects" of Bar and save them in a map the same way we can do it for a foo_factory. 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...

Edit2:

Additional constraints that proofed to be relevant during discussion:

  • Templates and template templates: The Foo are actually templates (with a single class argument) and the Bar are template templates taking a concrete Foo as template argument. The Foo templates have no specializations and all have the same "name", so querying any concrete type is fine. In particular SpecificFoo<double>::name is always valid. @Julius' answer has been extended to facilitate this already. For @Yakk's the same can probably be done (but it will take me some time for figure it out in detail).
  • Flexible bar factory code: The factory for Bar does a little more than just call the constructor. It also passes some arguments and does some type casting (in particular, it may have Foo references that should be dynamic_cast to the corresponding concrete derived Foo). Therefore a solution that allows to write this code inline during definition of the bar_factory seems most readable to me. @Julius' answer works great here, even if the loop code with tuples is a little verbose.
  • Making the "single place" listing the Foos even simpler: From the answers so far I believe the way to go for me is having a compile-time list of foo types and a way to iterate over them. There are two answers that define a list of Foo types (or templates) in one central place (either with a types template or with tuples), which is already great. However, for other reasons I already have in the same central place a list of macro calls, one for each foo, like DECLARE_FOO(FooA, "A") DECLARE_FOO(FooB, "B") .... Can the declaration of FooTypes be somehow take advantage of that, so I don't have to list them again? I guess such type lists cannot be declared iteratively (appending to an already existing list), or can it? In the absence of that, probably with some macro magic it would be possible. Maybe always redefining and thus appending to a preprocessor list in the DECLARE_FOO calls, and then finally some "iterate over loop" to define the FooTypes type list. IIRC boost preprocessor has facilities to loop over lists (although I don't want a boost dependency).

For some more context, you can think of the different Foo and it's template argument as classes similar to Eigen::Matrix<Scalar> and the Bar are cost functors to be used with Ceres. The bar factory returns objects like ceres::AutoDiffCostFunction<CostFunctor<SpecificFoo>, ...> as ceres::CostFunction* pointers.

Edit3:

Based on @Julius' answer I created a solution that works with Bars that are templates as well as template templates. I suspect one could unify bar_tmpl_factory and bar_ttmpl_factory into one function using variadic variadic template templates (is that a thing?).

run it

TODO:

  • combine bar_tmpl_factory and bar_ttmpl_factory
  • the point Making the "single place" listing the Foos even simpler from above
  • maybe replacing the use of tuples with @Yakk's types template (but in a way such that the creator function can be defined inline at the call site of the loop over all foo types).

I consider the question answered and if anything the above points should be separate questions.

4条回答
Deceive 欺骗
2楼-- · 2020-04-11 04:14

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;
    }();
    
查看更多
beautiful°
3楼-- · 2020-04-11 04:15

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;
}
查看更多
狗以群分
4楼-- · 2020-04-11 04:18

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;
}
查看更多
闹够了就滚
5楼-- · 2020-04-11 04:21
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.

查看更多
登录 后发表回答