使非constexpr积分值适应非类型模板参数,并使代码膨胀

时间:2014-02-21 18:14:21

标签: c++ c++11 constexpr inlining template-function

考虑使用F参数constexpr size_t

的函数对象I
struct F
{
    template <size_t I>
    constexpr size_t operator()(size <I>) const { return I; }
};

包含在size <I>类型中,其中(为简洁起见)

template <size_t N>
using size = std::integral_constant <size_t, N>;

当然,我们可以直接传递I,但我想通过使用它作为模板参数强调它是constexpr。函数F在这里是假的,但实际上它可以做各种有用的东西,比如从元组的I元素中检索信息。无论F如何,都假定I具有相同的返回类型。 I可以是任何整数类型,但假定为非负数。

问题

给定constexpr size_tI,我们可以通过

调用F
F()(size <I>());

现在,如果我们想要使用非constepr Fsize_t来呼叫i该怎么办?请考虑以下事项:

constexpr size_t L = 10;
idx <F, L> f;
for (size_t i = 0; i < L; ++i)
    cout << f(i) << " ";

(为什么我需要这个呢?为了给出一些上下文,我实际上是试图在一个容器视图中构建一个复合迭代器,它表示一系列“连接”(连接)的异构容器。这会给能够说join(a, b) = c;之类的数组join(a, b)c的长度相同。但是,i是迭代器状态所以不能是constexpr,而是迭代器存储在一个元组中,需要通过constexpr索引进行访问。单个value_type大致一致,以便联合视图可以采用common_type类型,但是容器和子迭代器的类型不同。)

解决方案

在这里,我提出了结构idx <F, L>,它为此目的调整函数F,假设输入参数小于L。这实际上编译了精细的输出

0 1 2 3 4 5 6 7 8 9 

这是一个live example

idx通过将输入i递归分解为二进制表示并重构constexpr对应N来工作:

template <typename F, size_t L, size_t N = 0, bool = (N < L)>
struct idx
{
    template <size_t R = 1>
    inline constexpr decltype(F()(size <0>()))
    operator()(size_t I, size <R> = size <R>()) const
    {
        return I%2 ? idx <F, L, N+R>()(--I/2, size <2*R>()) :
               I   ? idx <F, L, N  >()(  I/2, size <2*R>()) :
               F()(size <N>());
    }
};

其中R表示当前迭代时2的幂。为避免无限模板实例化,为N >= L提供了一个特化,将F()(size <0>())作为虚拟值返回:

template <typename F, size_t L, size_t N>
struct idx <F, L, N, false>
{
    template <size_t R>
    inline constexpr decltype(F()(size <0>()))
    operator()(size_t I, size <R>) const { return F()(size <0>()); }
};

实际上,这种方法是使用布尔参数更常见的习语的推广:

bool b = true;
b ? f <true>() : f <false>();

其中f是一个以bool为模板参数的函数。在这种情况下,很明显f的所有两个可能版本都被实例化了。

问题

虽然这有效并且其运行时复杂性可能在i中是对数的,但我关注编译时的含义,例如:

  • 实例化idx及其template operator()的多少个组合,以便在运行时为编译时未知的任何输入i工作? (我再次理解“所有可能的”,但有多少?)

  • 是否真的可以内联operator()

  • 是否存在“更容易”编译的替代策略或变体?

  • 我应该忘记这个想法作为纯code bloat的一个例子吗?

备注

以下是我为L的不同值测量的编译时间(以秒为单位)和可执行文件大小(以KB为单位):

 L      Clang(s)    GCC(s)    Clang(KB)    GCC(KB)
 10       1.3       1.7          33         36
 20       2.1       3.0          48         65
 40       3.7       5.8          87        120
 80       7.0      11.2         160        222
160      13.9      23.4         306        430
320      28.1      47.8         578        850
640      56.5     103.0        1126       1753

所以,虽然它在L中看起来大致呈线性,但是它很长很令人沮丧。

尝试强制operator()内联失败:可能被Clang忽略(可执行文件甚至更大),而GCC报告recursive inlining

在可执行文件上运行nm -C,例如对于L = 160,显示511/1253 operator()的不同版本(使用Clang / GCC)。这些都是针对N < L的,因此看起来终止专业化N >= L会被内联。

PS 即可。我会添加标签code-bloat,但系统不会让我。

4 个答案:

答案 0 :(得分:3)

我把这种技术称为魔术开关。

我知道这样做的最有效方法是建立自己的跳转表。

// first, index list boilerplate.  Does log-depth creation as well
// needed for >1000 magic switches:
template<unsigned...Is> struct indexes {typedef indexes<Is...> type;};
template<class lhs, class rhs> struct concat_indexes;
template<unsigned...Is, unsigned...Ys> struct concat_indexes<indexes<Is...>, indexes<Ys...>>{
    typedef indexes<Is...,Ys...> type;
};
template<class lhs, class rhs>
using ConcatIndexes = typename concat_indexes<lhs, rhs>::type;

template<unsigned min, unsigned count> struct make_indexes:
    ConcatIndexes<
        typename make_indexes<min, count/2>::type,
        typename make_indexes<min+count/2, (count+1)/2>::type
    >
{};
template<unsigned min> struct make_indexes<min, 0>:
    indexes<>
{};
template<unsigned min> struct make_indexes<min, 1>:
    indexes<min>
{};
template<unsigned max, unsigned min=0>
using MakeIndexes = typename make_indexes<min, max-min>::type;

// This class exists simply because [](blah){code}... `...` expansion
// support is lacking in many compilers:
template< typename L, typename R, unsigned I >
struct helper_helper {
    static R func( L&& l ) { return std::forward<L>(l)(size<I>()); }
};
// the workhorse.  Creates an "manual" jump table, then invokes it:
template<typename L, unsigned... Is>
auto
dispatch_helper(indexes<Is...>, L&& l, unsigned i)
-> decltype( std::declval<L>()(size<0>()) )
{
  // R is return type:
  typedef decltype( std::declval<L>()(size<0>()) ) R;
  // F is the function pointer type for our jump table:
  typedef R(*F)(L&&);
  // the jump table:
  static const F table[] = {
    helper_helper<L, R, Is>::func...
  };
  // invoke at the jump spot:
  return table[i]( std::forward<L>(l) );
};
// create an index pack for each index, and invoke the helper to
// create the jump table:
template<unsigned Max, typename L>
auto
dispatch(L&& l, unsigned i)
-> decltype( std::declval<L>()(size<0>()) )
{
  return dispatch_helper( MakeIndexes<Max>(), std::forward<L>(l), i );
};

需要一些静态设置,但运行时速度非常快。

i处于界限内的断言也可能有用。

live example

答案 1 :(得分:1)

如果您的解决方案限制了最大可能值(比如256),您可以使用宏魔术和switch语句来简化它:

#define POS(i) case (i): return F<(i)>(); break;
#define POS_4(i) POS(i + 0) POS(i + 1) POS(i + 2) POS(i + 3)
#define POS_16(i) POS_4(i + 0) POS_4(i + 4) POS_4(i + 8) POS_4(i + 12)

int func(int i)
{
    switch(i)
    {
        POS_16(0)
    }
}

另一种可能的解决方案是(使用C ++ 11)使用可变参数模板:

template<int I>
struct FF
{
    static int f() { return I; }
};


template<typename... I>
int func(int i)
{
    constexpr int (*Func[])() = { I::f... };
    return Func[i]();
}

int main(int argc, char** argv)
{
    func<FF<0>,FF<1>>(1);
}

答案 2 :(得分:0)

我会在这里采取明显的立场并询问是否&#34;我想通过将其用作模板参数来强调它是constexpr&#34;值得这个费用,如果:

struct F
{
    constexpr size_t operator()(size_t i) const { return i; }
    template <size_t I>
    constexpr size_t operator()(size <I>) const { return (*this)(I); }
};

不会是一个更简单的解决方案。

答案 3 :(得分:0)

这不完全是一个答案,我的问题仍然存在,但我找到了一个解决方案,在编译方面给人留下了深刻印象。这是问题中给出的解决方案的一个小调整,其中参数Roperator()移到外部idx,终止条件现在包括RN

template <
    typename F, size_t L,
    size_t R = 1, size_t N = 0, bool = (R < 2 * L && N < L)
>
struct idx //...

整个代码在新的live example中提供。

这种方法显然减少了R的大量不必要的专业化组合。编译时间和可执行文件大小急剧下降。例如,我能够使用Clang / GCC为L = 1<<12(4096)在10.7 / 18.7秒内编译,产生220/239 KB的可执行文件。在这种情况下,nm -C显示了operator()的546/250版本。