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

时间:2015-03-14 04:36:56

标签: c++ templates c++11 variadic-templates

说我有以下结构

#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和一个字节数组,然后对字节数组进行分块,将这些块作为函数的参数递归应用,直到应用所有参数,然后调用该函数。 / p>

我遇到的问题是它产生了很多模板。在调试模式下广泛使用时,这一点尤为明显 - 它会使二进制文件增长得非常快。

给出以下用例

#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 vs. 59.0 KiB,比原来大60%。

使用迭代替换递归是有意义的,类似于应用于可变参数Unpack<...>:unpack()的初始化列表,因此C ++每个类型只生成一个模板。

上面的代码编译得很好,如果你想稍微玩一下。

2 个答案:

答案 0 :(得分:3)

我写了一些疯狂的东西,模板和索引序列和元组完全受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。

在-O0使用std::function:〜3K,在-O3使用216B。

解压缩和调用的代码是几十个汇编指令。例如。, 使用gcc 4.9.2在x64上优化大小-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

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

编辑:推广到非POD。

deserializer中包含输入迭代器并使用转换运算符执行实际的解包是“聪明的” - 使用“聪明”的正面和负面内涵 - 但它不可扩展。客户端代码无法添加operator blahblah成员函数重载,控制转换运算符重载的唯一方法是使用大量的SFINAE。呸。因此,让我们放弃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);

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

技术上匹配任何 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参数,然后使用{ {1}}:

void

答案 1 :(得分:2)

首先,执行实际解包的功能。根据需要进行专业化。

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 - 只有递归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~8 KiB at -O0为〜1KiB。

index_sequence和朋友是C ++ 14的功能,但可以在C ++ 11中实现。在SO上有很多实现。对于C ++ 11,还要将tuple_element_t<...>替换为typename tuple_element<...>::type