gpt4 book ai didi

c++ - 将递归可变参数模板函数转换为迭代

转载 作者:太空宇宙 更新时间:2023-11-04 16:10:42 25 4
gpt4 key购买 nike

说我有以下结构

#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 ++每种类型只会生成一个模板。

如果您想稍微玩一下,上面的代码可以很好地编译。

最佳答案

我写了一些疯狂的东西,其中包含模板和索引序列以及完全受ranges-v3概念约束的元组,这很好。然后我想到,如果将参数直接解压缩到函数调用中,编译器将更容易优化。首先,我们创建一个类,该类可以从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*);


assembles to

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 $24, %rsp
ret


代码大小足够小,可以有效地内联,因此生成的模板数量不是一个因素。

编辑:泛化到非PODs。

deserializer中包装输入迭代器并使用转换运算符执行实际的拆包是“聪明的”(使用“聪明”的正负含义),但它不可扩展。客户端代码无法添加 operator blahblah成员函数重载,并且控制转换运算符重载的唯一方法是使用SFINAE堆。 uck因此,让我们放弃 deserializer的想法,并使用可扩展的分发机制。

首先,一个元函数剥离引用和​​cv限定词,以便我们可以例如当参数签名为 std::vector<double>时解压缩 const 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 {};


然后我们可以使用一个通用的参数unpack函数来执行标签分配:

template <typename T>
uncvref<T> unpack_arg(const std::uint8_t*& from) {
return unpack_arg(arg_tag<uncvref<T>>{}, from);


多亏了参数依赖查找的魔力,只要在使用前声明了 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;
}


从技术上讲,它匹配任何 arg_tag,但是如果匹配的类型不重要,则SFINAE将其从重载解析中删除。 (是的,我知道我之前说过POD。我改变了主意;琐碎的类型更加通用,仍然可以使用 memcpy。)这种调度机制的前端并没有太大变化:

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


正常工作不变。现在,我们可以通过为 unpack_arg重载 arg_tag<std::vector<T>>来提供解压缩向量的扩展:

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()>

现在的代码有一个问题:如果 fstd::function<void()>void(*)(void),则从 unpack推断出参数类型的 f重载将调用它们自己并无限递归。最简单的解决方法是命名该函数来完成解压缩不同内容的实际工作-我将选择 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.如果您更喜欢对于返回类型不是 void的函数产生编译错误,请删除从推论重载推论得出返回类型的 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);
}

关于c++ - 将递归可变参数模板函数转换为迭代,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/29045522/

25 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com