【问题标题】:Generic factory mechanism in C++17C++17 中的通用工厂机制
【发布时间】:2019-03-07 13:48:37
【问题描述】:

我想为一组派生类实现一个通用工厂机制,它不仅可以通用地实现工厂函数来创建该类的对象,还可以实现其他模板类的创建者,这些模板类的模板参数之一派生类。

理想情况下,解决方案将仅使用 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();
}

run it

我正在寻找一种机制,允许我在不列出所有类的情况下同时实现 foo_factorybar_factory,这样一旦我添加例如 FooD 作为额外的派生类,它们就不需要更新.理想情况下,不同的 Foo 衍生品会以某种方式“自我注册”,但将它们全部放在一个中心位置也是可以接受的。

编辑:

基于cmets/answers的一些澄清:

  • 在我的情况下,有必要使用(类似于)字符串来调用工厂,因为工厂的调用者使用 Foo / BarInterface 的多态性,即他们不知道具体的派生类。另一方面,在 Bar 中,我们希望使用派生 Foo 类的模板方法并促进内联,这就是为什么我们确实需要模板派生 Bar 类(而不是通过某些基类接口访问 Foo 对象)。
  • 我们可以假设所有派生的 Foo 类都定义在一个地方(因此,如果需要,我们可以在同一地方将它们全部列出一次的手动注册)。但是,他们不知道 Bar 的存在,实际上我们有多个不同的类,例如 BarInterfaceBar。因此,我们不能创建 Bar 的“构造函数对象”并将它们保存在映射中,就像我们为 foo_factory 所做的那样。我认为需要的是所有派生 Foo 类型的某种“编译时映射”(或列表),这样在定义 bar_factory 时,编译器可以迭代它们,但我不知道该怎么做...

编辑2:

证明相关的其他约束during discussion

  • 模板和模板模板: Foo 实际上是模板(具有单个类参数),而 Bar 是模板模板,将具体的 Foo 作为模板参数。 Foo 模板没有特化,并且都具有相同的“名称”,因此查询任何具体类型都可以。特别是SpecificFoo&lt;double&gt;::name 始终有效。 @Julius 的答案已经扩展以促进这一点。对于@Yakk,可能也可以这样做(但我需要一些时间来详细弄清楚)。
  • 灵活的条形工厂代码: Bar 的工厂不仅仅是调用构造函数。它还传递一些参数并进行一些类型转换(特别是,它可能有 Foo 引用,应该是 dynamic_cast 到相应的具体派生 Foo)。因此,允许在 bar_factory 定义期间内联编写此代码的解决方案对我来说似乎最易读。 @Julius 的回答在这里效果很好,即使带有元组的循环代码有点冗长。
  • 让列出 Foo 的“单一位置”更加简单: 从目前的答案来看,我相信我要走的路是拥有一个 foo 类型的编译时列表和一种迭代它们的方法。有两个答案在一个中心位置定义了 Foo 类型(或模板)列表(使用 types 模板或元组),这已经很棒了。但是,由于其他原因,我已经在同一个中心位置有一个宏调用列表,每个 foo 一个,例如DECLARE_FOO(FooA, "A") DECLARE_FOO(FooB, "B") ...FooTypes 的声明能否以某种方式利用这一点,所以我不必再次列出它们?我猜这样的类型列表不能迭代声明(附加到已经存在的列表),或者可以吗?在没有那个的情况下,可能有一些宏观魔法是可能的。也许总是重新定义并因此附加到DECLARE_FOO 调用中的预处理器列表,然后最后一些“迭代循环”来定义FooTypes 类型列表。 IIRC boost 预处理器具有循环列表的功能(尽管我不想要 boost 依赖项)。

对于更多 context,您可以将不同的 Foo 及其模板参数视为类似于 Eigen::Matrix&lt;Scalar&gt; 的类,而 Bar 是与 Ceres 一起使用的成本函子。 bar 工厂将ceres::AutoDiffCostFunction&lt;CostFunctor&lt;SpecificFoo&gt;, ...&gt; 之类的对象作为ceres::CostFunction* 指针返回。

编辑3:

根据@Julius 的回答,我创建了一个解决方案,该解决方案适用于作为模板和模板模板的 Bars。我怀疑可以使用可变参数模板模板将bar_tmpl_factorybar_ttmpl_factory 统一为一个函数(这是一回事吗?)。

run it

待办事项:

  • 结合bar_tmpl_factorybar_ttmpl_factory
  • 上面的点Making the "single place" listing the Foos even simpler
  • 可能会用@Yakk 的types 模板替换元组的使用(但在某种程度上,可以在循环的调用点内联定义创建者函数,覆盖所有 foo 类型)。

我认为问题已回答,如果有的话,以上几点应该是单独的问题。

【问题讨论】:

  • static std::map&lt;std::string, std::function&lt;std::unique_ptr&lt;Foo&gt;()&gt;&gt; 并在派生类附近注册?
  • 但是必须通过派生类的std::string 标识符吗?将其作为模板类型(foo_factory&lt;FooA&gt;()bar_factory&lt;FooA&gt;())传递怎么样?
  • @Jarod42,这适用于foo_factory,但不适用于bar_factory,对吧?请注意,有多个不同的 BarInterface 像我需要工厂的类。
  • @max66 是的,它需要处理诸如传递字符串(或者可能是多态 Foo* 指针)之类的东西。 bar_factory 的调用者不知道派生的 Foo 类。

标签: c++ templates polymorphism c++17 factory


【解决方案1】:
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;

Done

最初的设计实际上是带有 lambda 的 ,而不是 structs 的 shared_ptr_maker 等。

make_foosmake_bars 都具有零运行时状态。

【讨论】:

  • 谢谢!结果看起来非常优雅,非常惊人!我需要一些时间才能完全理解并在实际代码中融入我需要的内容。
  • 嗯...我以为我已经发表了以下评论,但它似乎消失了,或者我从未正确发布过:目前使用此解决方案,将无效名称传递给工厂会给您“最后" foo 在列表中键入。我想处理它的正确方法是增加type_switch 基本情况以同时检查op,如果它是错误的,请采取适当的措施(通过异常返回null,...),对吧?
  • @nikolaus 是的。或者有一个后备“默认”操作传递给它。
【解决方案2】:

我认为需要的是某种“编译时映射”(或列表) 所有派生的 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;
}

【讨论】:

  • 谢谢!这看起来很像我想要的。也许没有@Yakk 的回答那么优雅,但对我来说它更具可读性。编译时检查名称的唯一性会很有趣,但实际上不需要。如果违反这一点,其他事情也会很糟糕(而且 Foo 衍生物的数量并不多)。
  • 非常适合我的描述。我还有另一种情况,其中 Foo 派生类是模板类,而 Bar 是模板模板。我可以将 FooTypes 定义为模板没问题,但是当我尝试实现循环时,template&lt;class T&gt; using SpecificFoo = std::tuple_element_t&lt;i, FooTypes&lt;T&gt;&gt;; 不起作用,因为它处于块范围内。我能否以某种方式拥有一个模板元组并对其进行迭代(以一种我可以使用模板 SpecificFoo 实例化模板模板 Bar 的方式)?
  • @NikolausDemmel:我们可以假设模板模板参数匹配template&lt;class...&gt; class吗?如果他们有非类型参数,它就会变得棘手。但是,每个非类型参数都可以包装在一个类型中。
  • 是的,您实际上可以假设template&lt;class T&gt; struct FooA ...template&lt;template &lt;class&gt; class FooType1, template &lt;class&gt; class FooType2, ...&gt; Bar。 PS:... 只是随意的“等等”,而不是实际的 C++ 可变参数...
  • @NikolausDemmel:您可以将Foo 模板包装成template&lt;template&lt;class...&gt; class Tmpl&gt; struct TemplateTag {}; 形式的标签类型。然后,您可以将模板(由它们的标签表示)视为普通类型。同样,在std::integral_constant 中包装非类型模板参数(例如intstd::size_t)通常很有用。我尝试创建an example,但我不确定应该使用哪个模板参数,例如FooA 在询问其名称时。
【解决方案3】:

编写一个如下所示的通用工厂,允许在课堂网站上注册:

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)。这不是强依赖,可以通过在header中声明布尔值并在CPP中定义它们来摆脱它(这使得自注册更丑陋并且更容易注册失败)。

编辑

  1. 与其他答案相比,此答案的缺点是它在启动期间而不是在编译期间执行注册。另一方面,这使代码更加简单。
  2. 上面的例子假设Bar&lt;T&gt;的定义被移到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;
    }();
    

【讨论】:

  • 感谢您的回答!在我的情况下,这适用于foo_factory,但不适用于bar_factory,因为Bar 在定义Foo 的地方是未知的,并且有许多不同的类,如Bar,我会都需要独立注册。抱歉,我会在问题中澄清这个限制。
  • @NikolausDemmel 我添加了一条说明,说明可以在单独的函数中完成注册的方式
  • 感谢您的更新。这种情况更像是 Foo 在一个库中,而其他库中可能有许多不同的 BarInterface / Bar,所以当我添加或更改 Foo 时,我什至不知道我可能需要更新的所有地方。我同意他的代码要简单得多,这对我来说是一个有启发性的答案,但我认为我的其他一些答案更适合我的用例。无论如何,感谢您抽出宝贵的时间!
【解决方案4】:

在C++17中,我们可以应用折叠表达式来简化在这种情况下将生成函数std::make_unique&lt;FooA&gt;()std::make_unique&lt;FooB&gt;()等存储到工厂类中的过程。


首先,为方便起见,让我们定义以下类型别名Generator,它描述了每个生成函数的类型[](){ return std::make_unique&lt;T&gt;(); }

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

接下来,我们定义以下相当通用的仿函数createFactory,它将每个工厂返回为哈希映射std::unordered_map。 在这里,我使用逗号运算符应用折叠表达式。 例如,createFactory&lt;BarInterface, Bar, std::tuple&lt;FooA, FooB, FooC&gt;&gt;()() 返回与您的函数 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;
}

【讨论】:

  • 感谢您的回答!我非常喜欢它的通用性,它可以让我简化一些代码,但它还不是我理想中想要的。我不清楚对列出 Foos 的“中心位置”的限制(抱歉,我现在更新了问题)。这个中心位置应该在任何 Bars 的定义之前,即在您的情况下,Factory 模板类需要在不了解 Bar 的情况下工作(实际上有多个类,如 BarInterface / Bar我想为此创建类似bar_factory)。
  • 感谢您的更新。我现在看到了它是如何与只定义一次的 Foos 类型列表一起工作的。关于折叠表达式和逗号运算符的部分很有趣,我想我不会想到这一点!与 Julius 和 Yakk 的答案相比,这里的优势可能是通用代码更简单,如果您只想创建构造函数的运行时映射,然后您可以使用非模板代码进行操作。
  • @NikolausDemmel 再次感谢您。如果您想用FooAFooB、...和FooZ一次又一次地递归创建FooBarIF、...和BazIF的实例,哈希映射将提供一个性能有效的方法。 我认为我的答案的优势不仅在于可读性,还在于这一点。 由于至少在我的经验中类似的情况发生了很多,所以我只是对它做出了这个假设:)。如果没有,对不起。如果是这样,应避免冗长地编写每个映射定义。上面的函数可以简单的描述这些构造,所以我贴出来了。
  • 我现在明白了,如果你有很多 Foo 和不同种类的 BarIF,地图可以提供好处。但是,如果你想用 FooTypes 来实现,你仍然需要知道所有的 Foos 才能定义它。
猜你喜欢
  • 2010-12-17
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多