将递归可变参数模板函数转换为迭代
Convert a recursive variadic template function into iterative
假设我有以下结构
#include <functional>
template <typename ...T>
struct Unpack;
// specialization case for float
template <typename ...Tail>
struct Unpack<float, Tail...>
{
static void unpack(std::function<void(float, Tail...)> f, uint8_t *dataOffset)
{
float val;
memcpy(&val, dataOffset, sizeof(float));
auto g = [&](Tail&& ...args)
{
f(val, std::forward<Tail>(args)...);
};
Unpack<Tail...>::unpack(std::function<void(Tail...)>{g}, dataOffset + sizeof(float));
}
};
// base recursive case
template <typename Head, typename ... Tail>
struct Unpack<Head, Tail...>
{
static void unpack(std::function<void(Head, Tail...)> f, uint8_t *dataOffset)
{
Head val;
memcpy(&val, dataOffset, sizeof(Head));
auto g = [&](Tail&& ...args)
{
f(val, std::forward<Tail>(args)...);
};
Unpack<Tail...>::unpack(std::function<void(Tail...)>{g}, dataOffset + sizeof(Head));
}
};
// end of recursion
template <>
struct Unpack<>
{
static void unpack(std::function<void()> f, uint8_t *)
{
f(); // call the function
}
};
它所做的只是获取一个 std::function
和一个字节数组,并将字节数组分块,将这些块作为函数的参数递归地应用,直到应用所有参数,然后调用该函数.
我遇到的问题是它生成了很多模板。当在调试模式下广泛使用时,这一点尤其明显——它会导致二进制文件增长得非常快。
给定以下用例
#include <iostream>
#include <string.h>
using namespace std;
void foo1(uint8_t a, int8_t b, uint16_t c, int16_t d, uint32_t e, int32_t f, uint64_t g, int64_t h, float i, double j)
{
cout << a << "; " << b << "; " << c << "; " << d << "; " << e << "; " << f << "; " << g << "; " << h << "; " << i << "; " << j << endl;
}
void foo2(uint8_t a, int8_t b, uint16_t c, int16_t d, uint32_t e, int32_t f, int64_t g, uint64_t h, float i, double j)
{
cout << a << "; " << b << "; " << c << "; " << d << "; " << e << "; " << f << "; " << g << "; " << h << "; " << i << "; " << j << endl;
}
int main()
{
uint8_t *buff = new uint8_t[512];
uint8_t *offset = buff;
uint8_t a = 1;
int8_t b = 2;
uint16_t c = 3;
int16_t d = 4;
uint32_t e = 5;
int32_t f = 6;
uint64_t g = 7;
int64_t h = 8;
float i = 9.123456789;
double j = 10.123456789;
memcpy(offset, &a, sizeof(a));
offset += sizeof(a);
memcpy(offset, &b, sizeof(b));
offset += sizeof(b);
memcpy(offset, &c, sizeof(c));
offset += sizeof(c);
memcpy(offset, &d, sizeof(d));
offset += sizeof(d);
memcpy(offset, &e, sizeof(e));
offset += sizeof(e);
memcpy(offset, &f, sizeof(f));
offset += sizeof(f);
memcpy(offset, &g, sizeof(g));
offset += sizeof(g);
memcpy(offset, &h, sizeof(h));
offset += sizeof(h);
memcpy(offset, &i, sizeof(i));
offset += sizeof(i);
memcpy(offset, &j, sizeof(j));
std::function<void (uint8_t, int8_t, uint16_t, int16_t, uint32_t, int32_t, uint64_t, int64_t, float, double)> ffoo1 = foo1;
Unpack<uint8_t, int8_t, uint16_t, int16_t, uint32_t, int32_t, uint64_t, int64_t, float, double>::unpack(ffoo1, buff);
// uint64_t and in64_t are switched
//std::function<void (uint8_t, int8_t, uint16_t, int16_t, uint32_t, int32_t, int64_t, uint64_t, float, double)> ffoo2 = foo2;
//Unpack<uint8_t, int8_t, uint16_t, int16_t, uint32_t, int32_t, int64_t, uint64_t, float, double>::unpack(ffoo2, buff);
return 0;
}
我得到的带有注释的两行的调试二进制文件是 264.4 KiB,但是当我取消注释这两行时它变成 447.7 KiB,比原来的大 70%。
与发布模式相同:37.5 KiB 对比 59.0 KiB,比原来大 60%。
用迭代替换递归是有意义的,类似于应用于可变参数的初始化列表 Unpack<...>:unpack()
,这样 C++ 将只为每个类型生成一个模板。
上面的代码编译得很好,如果你想玩一下的话。
首先,一个执行实际解包的函数。根据需要进行专业化。
template<class T>
T do_unpack(uint8_t * data){
T val;
memcpy(&val, data, sizeof(T));
return val;
}
接下来,递归模板计算第 I
个元素的偏移量。这也可以写成迭代 C++14 constexpr
函数,但 GCC 4.9 不支持,并且似乎没有很好地优化非 constexpr
版本。并且 C++11 return
-only 递归 constexpr
感觉不值得为传统方法带来麻烦。
// compute the offset of the I-th element
template<size_t I, class T, class... Ts>
struct get_offset_temp {
static constexpr size_t value = get_offset_temp<I-1, Ts...>::value + sizeof(T);
};
template<class T, class... Ts>
struct get_offset_temp<0, T, Ts...>{
static constexpr size_t value = 0;
};
现在,使用计算的偏移量检索第 I
个参数的函数:
template<size_t I, class... Ts>
std::tuple_element_t<I, std::tuple<Ts...>> unpack_arg(uint8_t *data){
using T = std::tuple_element_t<I, std::tuple<Ts...>>;
return do_unpack<T>(data + get_offset_temp<I, Ts...>::value);
}
最后,解压参数并调用 function
的函数。为了避免不必要的 f
副本,我通过引用传递了它:
template<class... Ts, size_t... Is>
void unpack(const std::function<void(Ts...)> &f, uint8_t *dataOffset, std::index_sequence<Is...>){
f(unpack_arg<Is, Ts...>(dataOffset)...);
}
以及您调用的实际函数,它仅构造一个编译时整数序列并调用上面的函数:
template<class... Ts>
void unpack(std::function<void(Ts...)> f, uint8_t *dataOffset){
return unpack(f, dataOffset, std::index_sequence_for<Ts...>());
}
Demo.
一次调用和两次调用之间的二进制大小差异在 -O3
处约为 1KiB,而 ~8 KiB at -O0
。
index_sequence
和朋友是 C++14 的特性,但可以在 C++11 中实现。 SO 上有很多实现。对于 C++11,还将 tuple_element_t<...>
替换为 typename tuple_element<...>::type
.
我用模板、索引序列和元组写了一些疯狂的东西,这些东西完全受 ranges-v3 中概念的约束,这很好。然后我想到,如果将参数直接解压缩到函数调用中,编译器会更容易优化。首先,我们制作一个 class 可以从 char*
:
反序列化任何 POD 类型(可能放宽到可简单复制)
struct deserializer {
const std::uint8_t* in_;
deserializer(const std::uint8_t* in) : in_{in} {}
template <typename T>
operator T() {
static_assert(std::is_pod<T>(), "");
T t;
std::memcpy(&t, in_, sizeof(T));
in_ += sizeof(T);
return t;
}
};
然后您可以将 unpack
通常实现为:
template <typename...Ts, typename F>
void unpack(F&& f, const std::uint8_t* from) {
deserializer d{from};
std::forward<F>(f)(static_cast<Ts>(d)...); // Oops, broken.
}
除了它有未指定的行为,因为函数参数的顺序是未指定的。让我们引入一个类型来将参数转发给函数,这样我们就可以使用大括号初始化来强制从左到右求值:
struct forwarder {
template <typename F, typename...Ts>
forwarder(F&& f, Ts&&...ts) {
std::forward<F>(f)(std::forward<Ts>(ts)...);
}
};
// Requires explicit specification of argument types.
template <typename...Ts, typename F>
void unpack(F&& f, const std::uint8_t* from) {
deserializer d{from};
forwarder{std::forward<F>(f), static_cast<Ts>(d)...};
}
并引入一些特化来从函数指针和 std::function
中推导出参数类型,因此我们不必总是指定它们:
// Deduce argument types from std::function
template <typename R, typename...Args>
void unpack(std::function<R(Args...)> f, const std::uint8_t* from) {
unpack<Args...>(std::move(f), from);
}
// Deduce argument types from function pointer
template <typename R, typename...Args>
void unpack(R (*f)(Args...), const std::uint8_t* from) {
unpack<Args...>(f, from);
}
这一切都很好地暴露给了编译器并且非常可优化。单调用和双调用版本之间的二进制大小变化很小 (stealing T.C.'s framework):
使用函数指针:-O0 为 ~2K,-O3 为 64B。
使用 std::function
:-O0 为 ~3K,-O3 为 216B。
解包和调用的代码是几十条汇编指令。例如。,
x64 上的 gcc 4.9.2 使用 -Os
优化大小,显式专业化
template void unpack(decltype(foo1), const std::uint8_t*);
pushq %rax
movq %rsi, %rax
movswl 4(%rsi), %ecx
movzwl 2(%rsi), %edx
movq %rdi, %r10
movsbl 1(%rsi), %esi
movzbl (%rax), %edi
pushq 22(%rax)
pushq 14(%rax)
movl 10(%rax), %r9d
movl 6(%rax), %r8d
movsd 34(%rax), %xmm1
movss 30(%rax), %xmm0
call *(%r10)
addq , %rsp
ret
代码大小足够小,可以有效地内联,因此生成的模板数量不是一个因素。
编辑:推广到非PODs。
在 deserializer
中包装输入迭代器并使用转换运算符执行实际的解包是 "clever" - 使用 "clever" 的正面和负面内涵 - 但它是不可扩展。客户端代码不能添加 operator blahblah
成员函数重载,控制转换运算符重载的唯一方法是使用 SFINAE 堆。呸。所以让我们放弃deserializer
的想法,使用可扩展的调度机制。
首先,一个去除引用和 cv 限定符的元函数,这样我们就可以,例如当参数签名为 const std::vector<double>&
:
时解压 std::vector<double>
template <typename T>
using uncvref =
typename std::remove_cv<
typename std::remove_reference<T>::type
>::type;
我是标签分发的粉丝,所以设计一个可以容纳任何类型的标签包装器:
template <typename T> struct arg_tag {};
然后我们可以有一个执行标签分派的通用参数解包函数:
template <typename T>
uncvref<T> unpack_arg(const std::uint8_t*& from) {
return unpack_arg(arg_tag<uncvref<T>>{}, from);
多亏了 Argument Dependent Lookup 的魔力,只要在使用前声明了 在 之后声明的 unpack_arg
重载,就会找到调度程序的定义。即,调度系统易于扩展。我们将提供 POD 解包器:
template <typename T, typename std::enable_if<std::is_trivial<T>::value, int>::type = 0>
T unpack_arg(arg_tag<T>, const std::uint8_t*& from) {
T t;
std::memcpy(&t, from, sizeof(T));
from += sizeof(T);
return t;
}
技术上匹配 any arg_tag
,但如果匹配的类型不平凡,SFINAE 会将其从重载解析中删除。 (是的,我知道我之前说过 POD。我改变了主意;琐碎的类型更通用一些,仍然 memcpy
-able。)这个调度机制的前端没有太大变化:
struct forwarder {
template <typename F, typename...Args>
forwarder(F&& f, Args&&...args) {
std::forward<F>(f)(std::forward<Args>(args)...);
}
};
// Requires explicit specification of argument types.
template <typename...Ts, typename F>
void unpack(F&& f, const std::uint8_t* from) {
forwarder{std::forward<F>(f), unpack_arg<Ts>(from)...};
}
forwarder
没变,unpack<Types...>()
API用unpack_arg<Ts>(from)...
代替了static_cast<Ts>(d)...
,但显然还是一样的结构。类型推导重载:
template <typename R, typename...Args>
void unpack(std::function<R(Args...)> f, const std::uint8_t* from) {
unpack<Args...>(std::move(f), from);
}
template <typename R, typename...Args>
void unpack(R (*f)(Args...), const std::uint8_t* from) {
unpack<Args...>(f, from);
}
正常工作不变。现在我们可以通过为 arg_tag<std::vector<T>>
:
重载 unpack_arg
来提供解包向量的扩展
using vec_size_t = int;
template <typename T>
std::vector<T> unpack_arg(arg_tag<std::vector<T>>, const std::uint8_t*& from) {
std::vector<T> vec;
auto n = unpack_arg<vec_size_t>(from);
vec.reserve(n);
std::generate_n(std::back_inserter(vec), n, [&from]{
return unpack_arg<T>(from);
});
return vec;
}
请注意矢量解包重载如何通过调度程序解包其组件:unpack_arg<vec_size_t>(from)
用于大小,unpack_arg<T>(from)
用于每个元素。
再次编辑:std::function<void()>
现在代码有问题:如果 f
是 std::function<void()>
或 void(*)(void)
,那么从 f
推导参数类型的 unpack
重载将调用自己并无限递归。最简单的解决方法是命名执行解包实际工作的函数不同的东西——我会选择 unpack_explicit
——并让各种 unpack
前端调用它:
template <typename...Ts, typename F>
void unpack_explicit(F&& f, const std::uint8_t* from) {
forwarder{std::forward<F>(f), unpack_arg<Ts>(from)...};
}
// Requires explicit specification of argument types.
template <typename...Ts, typename F>
void unpack(F&& f, const std::uint8_t* from) {
unpack_explicit<Ts...>(std::forward<F>(f), from);
}
// Deduce argument types from std::function
template <typename R, typename...Args>
void unpack(std::function<R(Args...)> f, const std::uint8_t* from) {
unpack_explicit<Args...>(std::move(f), from);
}
// Deduce argument types from function pointer
template <typename R, typename...Args>
void unpack(R (*f)(Args...), const std::uint8_t* from) {
unpack_explicit<Args...>(f, from);
}
Here it is all put together. 如果您希望为具有 return 类型而不是 void
的函数获取编译错误,请删除推导 return 的 R
参数] 从推导重载中键入并简单地使用 void
:
// Deduce argument types from std::function
template <typename...Args>
void unpack(std::function<void(Args...)> f, const std::uint8_t* from) {
unpack_explicit<Args...>(std::move(f), from);
}
// Deduce argument types from function pointer
template <typename...Args>
void unpack(void (*f)(Args...), const std::uint8_t* from) {
unpack_explicit<Args...>(f, from);
}
假设我有以下结构
#include <functional>
template <typename ...T>
struct Unpack;
// specialization case for float
template <typename ...Tail>
struct Unpack<float, Tail...>
{
static void unpack(std::function<void(float, Tail...)> f, uint8_t *dataOffset)
{
float val;
memcpy(&val, dataOffset, sizeof(float));
auto g = [&](Tail&& ...args)
{
f(val, std::forward<Tail>(args)...);
};
Unpack<Tail...>::unpack(std::function<void(Tail...)>{g}, dataOffset + sizeof(float));
}
};
// base recursive case
template <typename Head, typename ... Tail>
struct Unpack<Head, Tail...>
{
static void unpack(std::function<void(Head, Tail...)> f, uint8_t *dataOffset)
{
Head val;
memcpy(&val, dataOffset, sizeof(Head));
auto g = [&](Tail&& ...args)
{
f(val, std::forward<Tail>(args)...);
};
Unpack<Tail...>::unpack(std::function<void(Tail...)>{g}, dataOffset + sizeof(Head));
}
};
// end of recursion
template <>
struct Unpack<>
{
static void unpack(std::function<void()> f, uint8_t *)
{
f(); // call the function
}
};
它所做的只是获取一个 std::function
和一个字节数组,并将字节数组分块,将这些块作为函数的参数递归地应用,直到应用所有参数,然后调用该函数.
我遇到的问题是它生成了很多模板。当在调试模式下广泛使用时,这一点尤其明显——它会导致二进制文件增长得非常快。
给定以下用例
#include <iostream>
#include <string.h>
using namespace std;
void foo1(uint8_t a, int8_t b, uint16_t c, int16_t d, uint32_t e, int32_t f, uint64_t g, int64_t h, float i, double j)
{
cout << a << "; " << b << "; " << c << "; " << d << "; " << e << "; " << f << "; " << g << "; " << h << "; " << i << "; " << j << endl;
}
void foo2(uint8_t a, int8_t b, uint16_t c, int16_t d, uint32_t e, int32_t f, int64_t g, uint64_t h, float i, double j)
{
cout << a << "; " << b << "; " << c << "; " << d << "; " << e << "; " << f << "; " << g << "; " << h << "; " << i << "; " << j << endl;
}
int main()
{
uint8_t *buff = new uint8_t[512];
uint8_t *offset = buff;
uint8_t a = 1;
int8_t b = 2;
uint16_t c = 3;
int16_t d = 4;
uint32_t e = 5;
int32_t f = 6;
uint64_t g = 7;
int64_t h = 8;
float i = 9.123456789;
double j = 10.123456789;
memcpy(offset, &a, sizeof(a));
offset += sizeof(a);
memcpy(offset, &b, sizeof(b));
offset += sizeof(b);
memcpy(offset, &c, sizeof(c));
offset += sizeof(c);
memcpy(offset, &d, sizeof(d));
offset += sizeof(d);
memcpy(offset, &e, sizeof(e));
offset += sizeof(e);
memcpy(offset, &f, sizeof(f));
offset += sizeof(f);
memcpy(offset, &g, sizeof(g));
offset += sizeof(g);
memcpy(offset, &h, sizeof(h));
offset += sizeof(h);
memcpy(offset, &i, sizeof(i));
offset += sizeof(i);
memcpy(offset, &j, sizeof(j));
std::function<void (uint8_t, int8_t, uint16_t, int16_t, uint32_t, int32_t, uint64_t, int64_t, float, double)> ffoo1 = foo1;
Unpack<uint8_t, int8_t, uint16_t, int16_t, uint32_t, int32_t, uint64_t, int64_t, float, double>::unpack(ffoo1, buff);
// uint64_t and in64_t are switched
//std::function<void (uint8_t, int8_t, uint16_t, int16_t, uint32_t, int32_t, int64_t, uint64_t, float, double)> ffoo2 = foo2;
//Unpack<uint8_t, int8_t, uint16_t, int16_t, uint32_t, int32_t, int64_t, uint64_t, float, double>::unpack(ffoo2, buff);
return 0;
}
我得到的带有注释的两行的调试二进制文件是 264.4 KiB,但是当我取消注释这两行时它变成 447.7 KiB,比原来的大 70%。
与发布模式相同:37.5 KiB 对比 59.0 KiB,比原来大 60%。
用迭代替换递归是有意义的,类似于应用于可变参数的初始化列表 Unpack<...>:unpack()
,这样 C++ 将只为每个类型生成一个模板。
上面的代码编译得很好,如果你想玩一下的话。
首先,一个执行实际解包的函数。根据需要进行专业化。
template<class T>
T do_unpack(uint8_t * data){
T val;
memcpy(&val, data, sizeof(T));
return val;
}
接下来,递归模板计算第 I
个元素的偏移量。这也可以写成迭代 C++14 constexpr
函数,但 GCC 4.9 不支持,并且似乎没有很好地优化非 constexpr
版本。并且 C++11 return
-only 递归 constexpr
感觉不值得为传统方法带来麻烦。
// compute the offset of the I-th element
template<size_t I, class T, class... Ts>
struct get_offset_temp {
static constexpr size_t value = get_offset_temp<I-1, Ts...>::value + sizeof(T);
};
template<class T, class... Ts>
struct get_offset_temp<0, T, Ts...>{
static constexpr size_t value = 0;
};
现在,使用计算的偏移量检索第 I
个参数的函数:
template<size_t I, class... Ts>
std::tuple_element_t<I, std::tuple<Ts...>> unpack_arg(uint8_t *data){
using T = std::tuple_element_t<I, std::tuple<Ts...>>;
return do_unpack<T>(data + get_offset_temp<I, Ts...>::value);
}
最后,解压参数并调用 function
的函数。为了避免不必要的 f
副本,我通过引用传递了它:
template<class... Ts, size_t... Is>
void unpack(const std::function<void(Ts...)> &f, uint8_t *dataOffset, std::index_sequence<Is...>){
f(unpack_arg<Is, Ts...>(dataOffset)...);
}
以及您调用的实际函数,它仅构造一个编译时整数序列并调用上面的函数:
template<class... Ts>
void unpack(std::function<void(Ts...)> f, uint8_t *dataOffset){
return unpack(f, dataOffset, std::index_sequence_for<Ts...>());
}
Demo.
一次调用和两次调用之间的二进制大小差异在 -O3
处约为 1KiB,而 ~8 KiB at -O0
。
index_sequence
和朋友是 C++14 的特性,但可以在 C++11 中实现。 SO 上有很多实现。对于 C++11,还将 tuple_element_t<...>
替换为 typename tuple_element<...>::type
.
我用模板、索引序列和元组写了一些疯狂的东西,这些东西完全受 ranges-v3 中概念的约束,这很好。然后我想到,如果将参数直接解压缩到函数调用中,编译器会更容易优化。首先,我们制作一个 class 可以从 char*
:
struct deserializer {
const std::uint8_t* in_;
deserializer(const std::uint8_t* in) : in_{in} {}
template <typename T>
operator T() {
static_assert(std::is_pod<T>(), "");
T t;
std::memcpy(&t, in_, sizeof(T));
in_ += sizeof(T);
return t;
}
};
然后您可以将 unpack
通常实现为:
template <typename...Ts, typename F>
void unpack(F&& f, const std::uint8_t* from) {
deserializer d{from};
std::forward<F>(f)(static_cast<Ts>(d)...); // Oops, broken.
}
除了它有未指定的行为,因为函数参数的顺序是未指定的。让我们引入一个类型来将参数转发给函数,这样我们就可以使用大括号初始化来强制从左到右求值:
struct forwarder {
template <typename F, typename...Ts>
forwarder(F&& f, Ts&&...ts) {
std::forward<F>(f)(std::forward<Ts>(ts)...);
}
};
// Requires explicit specification of argument types.
template <typename...Ts, typename F>
void unpack(F&& f, const std::uint8_t* from) {
deserializer d{from};
forwarder{std::forward<F>(f), static_cast<Ts>(d)...};
}
并引入一些特化来从函数指针和 std::function
中推导出参数类型,因此我们不必总是指定它们:
// Deduce argument types from std::function
template <typename R, typename...Args>
void unpack(std::function<R(Args...)> f, const std::uint8_t* from) {
unpack<Args...>(std::move(f), from);
}
// Deduce argument types from function pointer
template <typename R, typename...Args>
void unpack(R (*f)(Args...), const std::uint8_t* from) {
unpack<Args...>(f, from);
}
这一切都很好地暴露给了编译器并且非常可优化。单调用和双调用版本之间的二进制大小变化很小 (stealing T.C.'s framework):
使用函数指针:-O0 为 ~2K,-O3 为 64B。
使用 std::function
:-O0 为 ~3K,-O3 为 216B。
解包和调用的代码是几十条汇编指令。例如。,
x64 上的 gcc 4.9.2 使用 -Os
优化大小,显式专业化
template void unpack(decltype(foo1), const std::uint8_t*);
pushq %rax
movq %rsi, %rax
movswl 4(%rsi), %ecx
movzwl 2(%rsi), %edx
movq %rdi, %r10
movsbl 1(%rsi), %esi
movzbl (%rax), %edi
pushq 22(%rax)
pushq 14(%rax)
movl 10(%rax), %r9d
movl 6(%rax), %r8d
movsd 34(%rax), %xmm1
movss 30(%rax), %xmm0
call *(%r10)
addq , %rsp
ret
代码大小足够小,可以有效地内联,因此生成的模板数量不是一个因素。
编辑:推广到非PODs。
在 deserializer
中包装输入迭代器并使用转换运算符执行实际的解包是 "clever" - 使用 "clever" 的正面和负面内涵 - 但它是不可扩展。客户端代码不能添加 operator blahblah
成员函数重载,控制转换运算符重载的唯一方法是使用 SFINAE 堆。呸。所以让我们放弃deserializer
的想法,使用可扩展的调度机制。
首先,一个去除引用和 cv 限定符的元函数,这样我们就可以,例如当参数签名为 const std::vector<double>&
:
std::vector<double>
template <typename T>
using uncvref =
typename std::remove_cv<
typename std::remove_reference<T>::type
>::type;
我是标签分发的粉丝,所以设计一个可以容纳任何类型的标签包装器:
template <typename T> struct arg_tag {};
然后我们可以有一个执行标签分派的通用参数解包函数:
template <typename T>
uncvref<T> unpack_arg(const std::uint8_t*& from) {
return unpack_arg(arg_tag<uncvref<T>>{}, from);
多亏了 Argument Dependent Lookup 的魔力,只要在使用前声明了 在 之后声明的 unpack_arg
重载,就会找到调度程序的定义。即,调度系统易于扩展。我们将提供 POD 解包器:
template <typename T, typename std::enable_if<std::is_trivial<T>::value, int>::type = 0>
T unpack_arg(arg_tag<T>, const std::uint8_t*& from) {
T t;
std::memcpy(&t, from, sizeof(T));
from += sizeof(T);
return t;
}
技术上匹配 any arg_tag
,但如果匹配的类型不平凡,SFINAE 会将其从重载解析中删除。 (是的,我知道我之前说过 POD。我改变了主意;琐碎的类型更通用一些,仍然 memcpy
-able。)这个调度机制的前端没有太大变化:
struct forwarder {
template <typename F, typename...Args>
forwarder(F&& f, Args&&...args) {
std::forward<F>(f)(std::forward<Args>(args)...);
}
};
// Requires explicit specification of argument types.
template <typename...Ts, typename F>
void unpack(F&& f, const std::uint8_t* from) {
forwarder{std::forward<F>(f), unpack_arg<Ts>(from)...};
}
forwarder
没变,unpack<Types...>()
API用unpack_arg<Ts>(from)...
代替了static_cast<Ts>(d)...
,但显然还是一样的结构。类型推导重载:
template <typename R, typename...Args>
void unpack(std::function<R(Args...)> f, const std::uint8_t* from) {
unpack<Args...>(std::move(f), from);
}
template <typename R, typename...Args>
void unpack(R (*f)(Args...), const std::uint8_t* from) {
unpack<Args...>(f, from);
}
正常工作不变。现在我们可以通过为 arg_tag<std::vector<T>>
:
unpack_arg
来提供解包向量的扩展
using vec_size_t = int;
template <typename T>
std::vector<T> unpack_arg(arg_tag<std::vector<T>>, const std::uint8_t*& from) {
std::vector<T> vec;
auto n = unpack_arg<vec_size_t>(from);
vec.reserve(n);
std::generate_n(std::back_inserter(vec), n, [&from]{
return unpack_arg<T>(from);
});
return vec;
}
请注意矢量解包重载如何通过调度程序解包其组件:unpack_arg<vec_size_t>(from)
用于大小,unpack_arg<T>(from)
用于每个元素。
再次编辑:std::function<void()>
现在代码有问题:如果 f
是 std::function<void()>
或 void(*)(void)
,那么从 f
推导参数类型的 unpack
重载将调用自己并无限递归。最简单的解决方法是命名执行解包实际工作的函数不同的东西——我会选择 unpack_explicit
——并让各种 unpack
前端调用它:
template <typename...Ts, typename F>
void unpack_explicit(F&& f, const std::uint8_t* from) {
forwarder{std::forward<F>(f), unpack_arg<Ts>(from)...};
}
// Requires explicit specification of argument types.
template <typename...Ts, typename F>
void unpack(F&& f, const std::uint8_t* from) {
unpack_explicit<Ts...>(std::forward<F>(f), from);
}
// Deduce argument types from std::function
template <typename R, typename...Args>
void unpack(std::function<R(Args...)> f, const std::uint8_t* from) {
unpack_explicit<Args...>(std::move(f), from);
}
// Deduce argument types from function pointer
template <typename R, typename...Args>
void unpack(R (*f)(Args...), const std::uint8_t* from) {
unpack_explicit<Args...>(f, from);
}
Here it is all put together. 如果您希望为具有 return 类型而不是 void
的函数获取编译错误,请删除推导 return 的 R
参数] 从推导重载中键入并简单地使用 void
:
// Deduce argument types from std::function
template <typename...Args>
void unpack(std::function<void(Args...)> f, const std::uint8_t* from) {
unpack_explicit<Args...>(std::move(f), from);
}
// Deduce argument types from function pointer
template <typename...Args>
void unpack(void (*f)(Args...), const std::uint8_t* from) {
unpack_explicit<Args...>(f, from);
}