避免使用虚拟模板函数

Avoid virtual template functions

本文关键字:函数 虚拟      更新时间:2023-10-16

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

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

如果可能的话,我希望每个派生类能够在它自己的 .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");

}

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

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

这篇 SO 帖子展示了如何重载多个 lambda 或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" );

bob是运行时实际上是DerivedA<std::int8_t>Base<std::int8_t>unique_ptr

这可能有 tpyos。

这篇文章的大部分内容是元编程,用于设置单个对象,该对象通过tag_t<T1>重载每个对象tag_t<T0>而不重复我自己。 您可以为您的 4 种类型手动执行此操作。

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

最终用户学生只需要创建一个函数对象,该对象采用tag_t<X>const char*并返回unique_ptr<X>并且复制成本低廉,然后从中创建magic_factory(将其类型擦除为一堆std::function)。

描述库变为:

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_ttypes_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 次。


另一种方法是使用我的类型擦除类型擦除解决方案。

我们编写一组模板any_method,执行标记技巧来创建Base<A,B,C>对象。

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

然后我们从中继承并包装您的make以发送给那些any_method

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

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<T0, T1, T2>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