【问题标题】:Avoid virtual template functions避免使用虚拟模板函数
【发布时间】:2017-01-12 19:14:33
【问题描述】:

任何人都可以提出一种技术来避免以下代码中的虚拟模板函数吗?我已经阅读了其他几篇文章,但我不知道如何将这些解决方案应用于这种情况。

我正在构建一个包含模板类层次结构的库。我想创建一个“工厂”函数数组,可用于按名称实例化派生类(例如,基于命令行参数)。

如果可能的话,我希望每个派生类都能够在其自己的 .hpp 或 .cpp 文件中注册自己(而不是必须在某处维护所有可能派生类的单个列表)。

下面的代码几乎可以工作,除了尝试使用虚拟模板函数的致命缺陷。

//
// This code would appear in a library
//

template<class T>
class Base {
public:
  Base(const char* /*param*/) {}
};

//
// Description of each derived class.
// We need this non-templated base class so we can store all our
// descriptions in a single vector
//
class DescriptionBase {

private:
  const char *description;
  const char *name;

public:
  DescriptionBase(const char* pDesc, const char* pName) : description(pDesc), name(pName){
    // Whenever a Description object is created, it is automatically registered with the
    // global descriptionList.  This allows us to register derived classes in their own
    // .cpp/.hpp file (as opposed to keeping a centralized list of all derived classes).
    descriptionList.push_back(this);
  }

  // FAIL  Can't have virtual template functions
  virtual template<class T>
  Base<T> *make(const char *param) {return new Base<T>(param); }

  static vector<DescriptionBase *> descriptionList;
};

//global list of all derived classes
vector<DescriptionBase *> DescriptionBase::descriptionList;

// We use the template to store the type of the derived class 
// for use in the make method
template<template<typename> class D>
class Description : public DescriptionBase {

public:
  Description(const char* pDesc, const char* pName) : DescriptionBase(pDesc, pName) {}

  template<class T>
  Base<T> *make(const char *params) {
    return new D<T>(params);
  }
};

//
// These derived classes may be part of the library, or they may be
// written by users of the library.
//


template<class T>
class DerivedA : public Base<T> {
public:
  DerivedA(const char* param) : Base<T>(param) {return;}
};

Description<DerivedA> derivedA("derivedA", "This is the first derived class");

template<class T>
class DerivedB : public Base<T> {
  DerivedB(const char* param) : Base<T>(param) {return;}
};
Description<DerivedA> derivedB("derivedA", "This is the second derived class");

//
// Example code written by the user of the library.
//
//


int main(int argc, char *argv[]) {

  // Using a descriptionList is just a short-cut here.
  // Final code will use a map.
  int indexOfDerivedA = 0; 

  Base<int> *intItem = DescriptionBase::descriptionList[indexOfDerivedA]->make<int>("parameter to derived type's constructor");
  Base<char> *charItem = DescriptionBase::descriptionList[indexOfDerivedA]->make<char>("parameter to derived type's constructor");


}

【问题讨论】:

  • 如果你不知道返回值的类型,那么你就不能明智地使用构造函数,所以它们要么需要有一个共同的返回类型(这样你就可以将它实际分配给某物) 或者它们应该分别命名为工厂。类型系统无法知道“fred”或“george”是什么类型,但它必须在编译时确定调用是否有效。重要的是要记住 Base 和 Base 从类型系统的角度来看没有任何关系。
  • 我没有阅读您的代码,但您可以通过将字符串映射到返回与接口匹配的对象的函子来做到这一点。类通过在该映射中插入一个仿函数来注册自己。
  • @xaxxon 我修改了代码以使其更清晰。 “fred”和“george”是构造函数的参数,而不是类型名称。 Base 和 Base 不打算相关。我在库中构建descriptionList 时遇到了困难(当然,它不知道用户将选择哪些模板参数)。
  • @imreal 使用仿函数的问题是仿函数返回的对象是模板化的。该库可以定义函子,但它不能实例化它们,因为它不知道用户将选择哪些类型作为模板参数。
  • 是否有T 的有限有界列表可以传递给Base&lt;T&gt;?您能否至少模糊地描述您正在解决的实际实际问题,看看您是否只是在走死胡同?您想要按名称查找工厂模板吗?需要什么样的链接?我可以请求将多少代码放入常见的 .h 文件中? Base&lt;T&gt; 之间究竟有何不同?

标签: c++ templates factory virtual-functions


【解决方案1】:

这是一个编译时标签和类型:

template<class T>
struct tag_t { constexpr tag_t() {}; };
template<class T> constexpr tag_t tag<T>{};

这是一个类型列表:

template<class...>struct types_t{constexpr types_t(){}; using type=types_t;};
template<class...Ts>constexpr types_t<Ts...> types{};

这会映射类型列表的内容:

template<template<class...>class Z, class types>
struct fmap{};
template<template<class...>class Z, class types>
using fmap_t=typename fmap<Z,types>::type;
template<template<class...>class Z, template<class...>class types, class...Ts>
struct fmap<Z,types<Ts...>> {
  using type=types<Z<Ts...>>;
};

现在让我们列出一个工厂列表:

template<class...Args>
struct build_tagged_sig {
  template<class T>
  using result = std::unique_ptr<T>(tag_t<T>,Args...);
};
template<template<class...>class Out, class types, class...Args>
using tagged_factories =
  fmap_t<
    std::function,
    fmap_t<
      build_tagged_sig<Args...>::template result,
      fmap_t< Out, types >
    >
  >;

这会将types 应用于模板:

template<template<class...>class Z, class types>
struct apply_types {};
template<template<class...>class Z, class types>
using apply_types_t = typename apply_types<Z,types>::type;
template<template<class...>class Z, template<class...>class types, class...Ts>
struct apply_types<Z, types<Ts...>> {
  using type=Z<Ts...>;
};
template<template<class...>class Z>
struct applier {
  template<class types>
  using result=apply_types_t<Z,types>;
};

This SO post shows how to overload multiple lambdas or std::functions.

using my_types = types_t< std::int8_t, std::int16_t, std::int32_t, std::int64_t >;

using magic_factory =
  apply_types_t<
    overload,
    tagged_factories< Base, my_types, const char* > >
  >;

这是一个重载

std::function< std::unique_ptr<Base<std::int8_t>>( tag_t<Base<std::int8_t>>, const char* ) >,
std::function< std::unique_ptr<Base<std::int16_t>>( tag_t<Base<std::int16_t>>, const char* ) >,
std::function< std::unique_ptr<Base<std::int32_t>>( tag_t<Base<std::int32_t>>, const char* ) >,
std::function< std::unique_ptr<Base<std::int64_t>>( tag_t<Base<std::int64_t>>, const char* ) >

我们现在写一个寄存器工厂:

template<class F, class...Ts>
magic_factory make_poly_factory( types_t<Ts...>, F&& f ) {
  return magic_factory(
    (void(tag<Ts>), f)...
  );
}
template<class F>
magic_factory make_poly_factory( F&& f ) {
  return make_poly_factory( my_types{}, f );
}

它创建了 N 个 f 副本并将每个副本存储在一个 std::function 中,全部存储在一个对象中。

取返回值,你可以通过重载决议调用一个单独的。

template<class T>
std::unique_ptr<Base<T>> factory_A_impl( tag_t<Base<T>>, const char* param) {
  return new DerivedA<T>(param);
}
auto magic = magic_factory( my_types{}, [](auto tag, const char* param){
  return factory_A_impl( tag, param );
});

std::unique_ptr<Base<std::int8_t>> bob = magic( tag<std::int8_t>, "hello" );

bobunique_ptrBase&lt;std::int8_t&gt;,而Base&lt;std::int8_t&gt; 在运行时实际上是DerivedA&lt;std::int8_t&gt;

这可能有 tpyos。

这篇文章的大部分内容是元编程来设置单个对象,该对象重载tag_t&lt;T0&gt;tag_t&lt;T1&gt; 中的每一个,而无需重复我自己。您可以为您的 4 种类型手动执行此操作。

我使用的重载假定我们没有采用带有模板参数的单个 lambda,并且类型擦除每个重载,而是一组 lambda。第一个会让一些上面的事情变得更容易。

最终用户学生只需要创建一个函数对象,它接受一个 tag_t&lt;X&gt; 和一个 const char* 并返回一个 unique_ptr&lt;X&gt; 并且复制成本低,然后从中创建一个 magic_factory(该类型- 将其擦除成一束std::functions)。

DescriptionBase 变为:

struct Description {
  char const* description = 0;
  char const* name = 0;
  magic_factory factory;
  template<class T>
  std::unique_ptr<Base<T>>
  make(const char *param) {
    return factory( tag<T>, param );
  }
};

多态性现在在magic_factory 内。存储Description 的实例,而不是指向它们的指针,因为它们是值类型多态性。

看,很简单。

添加更多模板参数只会增加fmap 的复杂性,并增加magic_factory 的创建者的责任。

您需要一个叉积运算来从一个包含 4 个元素的列表中生成 64 组不同的类型。它将是types_t 中的types_t

打电话给my_many_types

然后

using magic_factory =
  apply_types_t<
    overload,
    tagged_factories< applier<Base>::template result, my_types, const char* > >
  >;

完成了,我们现在有 64 个带有如下签名的重载:

std::unique_ptr<Base<std::int8_t, std::int16_t, std::int8_t>>(
  tag_t<Base<std::int8_t, std::int16_t, std::int8_t>>,
  const char*
)

现在我们可以手动完成所有这些操作。

建立一个这样的表:

template<class T>
using factory_ptr = std::unique_ptr<T>( void*, tag_t<T>, const char* );

using factory_table = std::tuple<
  factory_ptr< Base< std::int8_t, std::int8_t, std::int8_t > >,
  factory_ptr< Base< std::int8_t, std::int8_t, std::int16_t > >,
  factory_ptr< Base< std::int8_t, std::int8_t, std::int32_t > >,
  factory_ptr< Base< std::int8_t, std::int8_t, std::int64_t > >,
  factory_ptr< Base< std::int8_t, std::int16_t, std::int8_t > >,
  factory_ptr< Base< std::int8_t, std::int16_t, std::int16_t > >,
  factory_ptr< Base< std::int8_t, std::int16_t, std::int32_t > >,
  factory_ptr< Base< std::int8_t, std::int16_t, std::int64_t > >,

...

  factory_ptr< Base< std::int64_t, std::int64_t, std::int64_t > >
>;

现在是魔法工厂:

struct magic_factory {
  std::unique_ptr<void, void(*)(void*)> state;
  factory_table table;

  template<class T0, class T1, class T2>
  std::unique_ptr<Base<T0, T1, T2>> make( char const* param ) {
    auto f = std::get<factory_ptr< Base< T0, T1, T2 > >>( table );
    return f( state.get(), param );
  }

  magic_factory(magic_factory&&)=default;
  template<class T,
    class=std::enable_if_t<!std::is_same<std::decay_t<T>, magic_factory>::value>
  >
  magic_factory( T&& t ) {
    ptr = {new std::decay_t<T> >(std::forward<T>(t)),
      [](void* ptr){
        delete static_cast< std::decay_t<T>* >(ptr);
      }
    };
    // 64 lines like this:
    std::get<factory_ptr< Base< std::int8_t, std::int8_t, std::int8_t > >>( table )
    = +[](void* pvoid, tag_t<Base< std::int8_t, std::int8_t, std::int8_t >> tag, char const* param)->std::unique_ptr<Base< std::int8_t, std::int8_t, std::int8_t >>
    {
      auto*pt = static_cast<std::decay_t<T>*>(pvoid);
      return (pt)(tag, param);
    };
  }
};

我们在这里构建了一个 tuple 类型擦除函数和一个指向其参数的 void 指针并调度我们自己。

您也可以使用上述一些机制来自动执行此操作。手动表确实提高了效率,因为我们不会像 std::function 版本那样将可调用对象的状态复制 N 次。


另一种方法是使用my type erasing type erasure 解决方案。

我们编写了一组模板any_methods,通过标签技巧创建Base&lt;A,B,C&gt;对象。

然后我们在每个any_methods 上创建一个super_any

然后我们从它继承并包装你的make 以发送给那些any_methods。

这可能与上面的手动方法一样有效。

template<class T0, class T1, class T2>
auto make_base = make_any_method<std::unique_ptr<Base<T0, T1, T2>>(char const* name)>(
  [](auto* p, char const* name)
  {
    return p->make<T0, T1, T2>( name );
  }
);
template<class T0, class T1, class T2>
using base_maker = decltype(make_base);

现在我们的工厂是:

super_any< base_maker<std::int8_t, std::int8_t, std::int8_t > > bob;

bob 可以存储一个指向实现make&lt;T0, T1, T2&gt; 的类的指针,它们与int8_t, int8_t, int8_t 匹配。

再添加 64 行就完成了。

bob 存储的类型根本不需要公共基类。它不使用 C++ 继承来实现多态性,而是手动类型擦除。

随便叫它

auto r = (bob->*make_base<std::int8_t, std::int8_t, std::int8_t>)("hello");

自然而然地将更多类型传递给super_any,它支持更多类型的make_base

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2023-02-10
    • 2011-06-15
    • 2011-10-09
    • 2017-06-10
    • 1970-01-01
    • 1970-01-01
    • 2018-02-01
    相关资源
    最近更新 更多