我正在研究在现代C ++(C ++ 11 / C ++ 14)中动态调度不相关类型的可能实现。
通过“动态分派类型”我的意思是在运行时我们需要通过其整数索引从列表中选择一个类型并用它做一些事情(调用静态方法,使用类型特征等)。< / p>
例如,考虑序列化数据流:有几种数据值,它们以不同方式序列化/反序列化;有几个编解码器,它们进行序列化/反序列化;我们的代码从流中读取类型标记,然后决定它应该调用哪个编解码器来读取完整值。
我对许多操作感兴趣,这些操作可以在类型上调用(几个静态方法,类型特征......),并且可以是从逻辑类型到C ++类的不同映射,而不仅仅是1: 1(在带有序列化的示例中,它表示可能有多种数据类型都由同一个编解码器序列化)。
我还希望避免手动代码重复,并使代码更易于维护且不易出错。表现也很重要。
目前我正在看到那些可能的实现,我错过了什么?这可以做得更好吗?
使用switch-case手动编写尽可能多的函数,因为类型上可能有操作调用。
size_t serialize(const Any & any, char * data)
{
switch (any.type) {
case Any::Type::INTEGER:
return IntegerCodec::serialize(any.value, data);
...
}
}
Any deserialize(const char * data, size_t size)
{
Any::Type type = deserialize_type(data, size);
switch (type) {
case Any::Type::INTEGER:
return IntegerCodec::deserialize(data, size);
...
}
}
bool is_trivially_serializable(const Any & any)
{
switch (any.type) {
case Any::Type::INTEGER:
return traits::is_trivially_serializable<IntegerCodec>::value;
...
}
}
优点:简单易懂;编译器可以内联调度方法。
缺点:需要大量手动重复(或外部工具生成代码)。
像这样创建调度表
class AnyDispatcher
{
public:
virtual size_t serialize(const Any & any, char * data) const = 0;
virtual Any deserialize(const char * data, size_t size) const = 0;
virtual bool is_trivially_serializable() const = 0;
...
};
class AnyIntegerDispatcher: public AnyDispatcher
{
public:
size_t serialize(const Any & any, char * data) const override
{
return IntegerCodec::serialize(any, data);
}
Any deserialize(const char * data, size_t size) const override
{
return IntegerCodec::deserialize(data, size);
}
bool is_trivially_serializable() const
{
return traits::is_trivially_serializable<IntegerCodec>::value;
}
...
};
...
// global constant
std::array<AnyDispatcher *, N> dispatch_table = { new AnyIntegerDispatcher(), ... };
size_t serialize(const Any & any, char * data)
{
return dispatch_table[any.type]->serialize(any, data);
}
Any deserialize(const char * data, size_t size)
{
return dispatch_table[any.type]->deserialize(data, size);
}
bool is_trivially_serializable(const Any & any)
{
return dispatch_table[any.type]->is_trivially_serializable();
}
优点:它更灵活一点 - 需要为每个调度类型编写一个调度程序类,但是可以将它们组合在不同的调度表中。
缺点:它需要编写大量的调度代码。由于虚拟调度以及将编解码器的方法内联到调用者站点不可能存在一些开销。
使用模板化调度功能
template <typename F, typename... Args>
auto dispatch(Any::Type type, F f, Args && ...args)
{
switch (type) {
case Any::Type::INTEGER:
return f(IntegerCodec(), std::forward<Args>(args)...);
...
}
}
size_t serialize(const Any & any, char * data)
{
return dispatch(
any.type,
[] (const auto codec, const Any & any, char * data) {
return std::decay_t<decltype(codec)>::serialize(any, data);
},
any,
data
);
}
bool is_trivially_serializable(const Any & any)
{
return dispatch(
any.type,
[] (const auto codec) {
return traits::is_trivially_serializable<std::decay_t<decltype(codec)>>::value;
}
);
}
优点:它只需要一个switch-case调度函数和每个操作调用中的一些代码(至少手动编写)。编译器可以内联它认为合适的内容。
缺点:它更复杂,需要C ++ 14(如此干净和紧凑)并依赖于编译器能力来优化掉未使用的编解码器实例(仅用于选择正确的重载编解码器)。
对于一组逻辑类型,可能有几个映射到实现类(本例中的编解码器),最好是概括解决方案#3并编写完全通用的调度函数,它们接收编译时映射类型值和调用类型之间。像这样:
template <typename Mapping, typename F, typename... Args>
auto dispatch(Any::Type type, F f, Args && ...args)
{
switch (type) {
case Any::Type::INTEGER:
return f(mpl::map_find<Mapping, Any::Type::INTEGER>(), std::forward<Args>(args)...);
...
}
}
我倾向于解决方案#3(或#4)。但我确实想知道 - 是否可以避免手动编写dispatch
函数?我的意思是它的开关盒。这个switch-case完全来自类型值和类型之间的编译时映射 - 是否有任何方法可以处理它对编译器的生成?
答案 0 :(得分:5)
标记调度,传递类型以选择重载,是有效的。 std
库通常将它用于迭代器上的算法,因此不同的迭代器类别会得到不同的实现。
当我有一个类型ID列表时,我确保它们是连续的并写一个跳转表。
这是指向完成手头任务的函数的指针数组。
您可以使用C ++ 11或更高版本自动编写它;我把它称为magic switch,因为它就像一个运行时开关,它调用一个基于运行时的编译时间值的函数。我使用lambdas创建函数,并在其中展开参数包以使它们的主体不同。然后他们调度到传入的函数对象。
写下来,然后你可以将你的序列化/反序列化代码移到&#34; type safe&#34;码。使用traits从编译时索引映射到类型标记,和/或根据索引将调度分配到重载函数。
这是一个C ++ 14魔术开关:
template<std::size_t I>using index=std::integral_constant<std::size_t, I>;
template<class F, std::size_t...Is>
auto magic_switch( std::size_t I, F&& f, std::index_sequence<Is...> ) {
auto* pf = std::addressof(f);
using PF = decltype(pf);
using R = decltype( (*pf)( index<0>{} ) );
using table_entry = R(*)( PF );
static const table_entry table[] = {
[](PF pf)->R {
return (*pf)( index<Is>{} );
}...
};
return table[I](pf);
}
template<std::size_t N, class F>
auto magic_switch( std::size_t I, F&& f ) {
return magic_switch( I, std::forward<F>(f), std::make_index_sequence<N>{} );
}
使用看起来像:
std::size_t r = magic_switch<100>( argc, [](auto I){
return sizeof( char[I+1] ); // I is a compile-time size_t equal to argc
});
std::cout << r << "\n";
如果您可以在编译时注册类型枚举以输入map(通过类型特征或其他类型),则可以通过魔术开关往返,将运行时枚举值转换为编译时类型标记。
template<class T> struct tag_t {using type=T;};
然后你可以像这样编写序列化/反序列化:
template<class T>
void serialize( serialize_target t, void const* pdata, tag_t<T> ) {
serialize( t, static_cast<T const*>(pdata) );
}
template<class T>
void deserialize( deserialize_source s, void* pdata, tag_t<T> ) {
deserialize( s, static_cast<T*>(pdata) );
}
如果我们有enum DataType
,我们会写一个特征:
enum DataType {
Integer,
Real,
VectorOfData,
DataTypeCount, // last
};
template<DataType> struct enum_to_type {};
template<DataType::Integer> struct enum_to_type:tag_t<int> {};
// etc
void serialize( serialize_target t, Any const& any ) {
magic_switch<DataType::DataTypeCount>(
any.type_index,
[&](auto type_index) {
serialize( t, any.pdata, enum_to_type<type_index>{} );
}
};
}
所有繁重的工作现在由enum_to_type
特征类专精,DataType
枚举和表格的重载完成:
void serialize( serialize_target t, int const* pdata );
是类型安全的。
请注意,您的any
实际上不是any
,而是variant
。它包含一个有界的类型列表,而不是任何内容。
此magic_switch
最终用于重新实现std::visit
功能,这也为您提供了对variant
中存储的类型的类型安全访问。
如果您希望它包含任何,您必须确定要支持的操作,为它在any
中存储时运行的类型擦除代码编写,将类型擦除的操作存储在数据旁边,bob是你的叔叔。
答案 1 :(得分:0)
这是在#3和#4之间的某个解决方案。也许它给了一些灵感,不确定它是否真的有用。
您可以直接使用&#34;编解码器&#34;而不是使用接口基类和虚拟调度。编码成一些不相关的特质结构:
struct AnyFooCodec
{
static size_t serialize(const Any&, char*)
{
// ...
}
static Any deserialize(const char*, size_t)
{
// ...
}
static bool is_trivially_serializable()
{
// ...
}
};
struct AnyBarCodec
{
static size_t serialize(const Any&, char*)
{
// ...
}
static Any deserialize(const char*, size_t)
{
// ...
}
static bool is_trivially_serializable()
{
// ...
}
};
然后您可以将这些特征类型放入类型列表中,这里我只使用std::tuple
:
typedef std::tuple<AnyFooCodec, AnyBarCodec> DispatchTable;
现在我们可以编写一个通用的调度函数,将第n个类型的特征传递给给定的仿函数:
template <size_t N>
struct DispatchHelper
{
template <class F, class... Args>
static auto dispatch(size_t type, F f, Args&&... args)
{
if (N == type)
return f(typename std::tuple_element<N, DispatchTable>::type(), std::forward<Args>(args)...);
return DispatchHelper<N + 1>::dispatch(type, f, std::forward<Args>(args)...);
}
};
template <>
struct DispatchHelper<std::tuple_size<DispatchTable>::value>
{
template <class F, class... Args>
static auto dispatch(size_t type, F f, Args&&... args)
{
// TODO: error handling (type index out of bounds)
return decltype(DispatchHelper<0>::dispatch(type, f, args...)){};
}
};
template <class F, class... Args>
auto dispatch(size_t type, F f, Args&&... args)
{
return DispatchHelper<0>::dispatch(type, f, std::forward<Args>(args)...);
}
这使用线性搜索来找到合适的特征,但是通过一些努力,至少可以使其成为二元搜索。此外,编译器应该能够内联所有代码,因为不涉及虚拟分派。也许编译器甚至足够智能,基本上可以把它变成一个开关。