【问题标题】:Getting template metaprogramming compile-time constants at runtime在运行时获取模板元编程编译时常量
【发布时间】:2010-10-28 20:08:33
【问题描述】:

背景

考虑以下几点:

template <unsigned N>
struct Fibonacci
{
    enum
    {
        value = Fibonacci<N-1>::value + Fibonacci<N-2>::value
    };
};

template <>
struct Fibonacci<1>
{
    enum
    {
        value = 1
    };
};

template <>
struct Fibonacci<0>
{
    enum
    {
        value = 0
    };
};

这是一个常见的例子,我们可以将斐波那契数的值作为编译时常量:

int main(void)
{
    std::cout << "Fibonacci(15) = ";
    std::cout << Fibonacci<15>::value;
    std::cout << std::endl;
}

但你显然无法在运行时获取值:

int main(void)
{
    std::srand(static_cast<unsigned>(std::time(0)));

    // ensure the table exists up to a certain size
    // (even though the rest of the code won't work)
    static const unsigned fibbMax = 20;
    Fibonacci<fibbMax>::value;

    // get index into sequence
    unsigned fibb = std::rand() % fibbMax;

    std::cout << "Fibonacci(" << fibb << ") = ";
    std::cout << Fibonacci<fibb>::value;
    std::cout << std::endl;
}

因为 fibb 不是编译时常量。

问题

所以我的问题是:

在运行时查看此表的最佳方法是什么?最明显的解决方案(“解决方案”应该轻描淡写)是有一个大的 switch 语句:

unsigned fibonacci(unsigned index)
{
    switch (index)
    {
    case 0:
        return Fibonacci<0>::value;
    case 1:
        return Fibonacci<1>::value;
    case 2:
        return Fibonacci<2>::value;
    .
    .
    .
    case 20:
        return Fibonacci<20>::value;
    default:
        return fibonacci(index - 1) + fibonacci(index - 2);
    }
}

int main(void)
{
    std::srand(static_cast<unsigned>(std::time(0)));

    static const unsigned fibbMax = 20;    

    // get index into sequence
    unsigned fibb = std::rand() % fibbMax;

    std::cout << "Fibonacci(" << fibb << ") = ";
    std::cout << fibonacci(fibb);
    std::cout << std::endl;
}

但是现在表格的大小是非常硬编码的,将其扩展为 40 并不容易。

我想出的唯一一个具有类似查询方法的是:

template <int TableSize = 40>
class FibonacciTable
{
public:
    enum
    {
        max = TableSize
    };

    static unsigned get(unsigned index)
    {
        if (index == TableSize)
        {
            return Fibonacci<TableSize>::value;
        }
        else
        {
            // too far, pass downwards
            return FibonacciTable<TableSize - 1>::get(index);
        }
    }
};

template <>
class FibonacciTable<0>
{
public:
    enum
    {
        max = 0
    };

    static unsigned get(unsigned)
    {
        // doesn't matter, no where else to go.
        // must be 0, or the original value was
        // not in table
        return 0;
    }
};

int main(void)
{
    std::srand(static_cast<unsigned>(std::time(0)));

    // get index into sequence
    unsigned fibb = std::rand() % FibonacciTable<>::max;

    std::cout << "Fibonacci(" << fibb << ") = ";
    std::cout << FibonacciTable<>::get(fibb);
    std::cout << std::endl;
}

这似乎工作得很好。我看到的唯一两个问题是:

  • 调用堆栈可能很大,因为计算斐波那契 需要我们通过 TableMax 一直到 2,并且:

  • 如果值在表之外,则返回零而不是计算它。

那么我有什么遗漏吗?似乎应该有更好的方法在运行时挑选出这些值。

可能是 switch 语句的模板元编程版本,它可以生成最多一定数量的 switch 语句?

提前致谢。

【问题讨论】:

  • 请问您为什么要这样做? :)
  • 好奇心。没有人会因为不编程或不思考而在编程或思考方面变得更好。
  • 等等...您希望您的编译时算法使用您在运行时获得的输入来计算结果?这不是需要一台时光机吗?
  • 无论如何,你知道的。使用 C++1x,您可以编写 "constexpr int fib(int n) { return (n
  • 很好的例子!但有些人只是倾向于用模板做过于聪明的事情。当然,这个案例很有趣,也很有启发性,但对于一个真实的项目,我宁愿把它编码为一个记忆或初始化时创建的查找表。想想看,这个例子并没有减少代码大小,而且增加了一些关于正在发生的事情的模糊性。当然很清楚,因为“斐波那契”这个词很容易解释它的作用。但想象一个更复杂的场景。看到它是一个查找表并非易事。

标签: c++ templates runtime metaprogramming


【解决方案1】:
template <unsigned long N>
struct Fibonacci
{
    enum
    {
        value = Fibonacci<N-1>::value + Fibonacci<N-2>::value
    };
    static void add_values(vector<unsigned long>& v)
    {
        Fibonacci<N-1>::add_values(v);
        v.push_back(value);
    }
};

template <>
struct Fibonacci<0>
{
    enum
    {
        value = 0
    };
    static void add_values(vector<unsigned long>& v)
    {
        v.push_back(value);
    }

};

template <>
struct Fibonacci<1>
{
    enum
    {
        value = 1
    };
    static void add_values(vector<unsigned long>& v)
    {
        Fibonacci<0>::add_values(v);
        v.push_back(value);
    }
};



int main()
{
    vector<unsigned long> fibonacci_seq;
    Fibonacci<45>::add_values(fibonacci_seq);
    for (int i = 0; i <= 45; ++i)
        cout << "F" << i << " is " << fibonacci_seq[i] << '\n';
}

经过深思熟虑,我想出了这个解决方案。当然,您仍然需要在运行时将值添加到容器中,但(重要的是)它们不是在运行时计算

附带说明,重要的是不要在Fibonacci&lt;0&gt; 之上定义Fibonacci&lt;1&gt;,否则编译器在解析对Fibonacci&lt;0&gt;::add_values 的调用时会非常感到困惑,因为Fibonacci&lt;0&gt;' s 模板特化尚未指定。

当然,TMP 有其局限性:您需要预先计算的最大值,并且在运行时获取值需要递归(因为模板是递归定义的)。

【讨论】:

  • +1 很好,解决这个问题的一般方法。关于 VS 的翻转:这是因为 14.7.3/6 :) “如果模板、成员模板或类模板的成员被明确专门化,则应在第一次使用之前声明该专门化将导致隐式实例化发生的专业化,在发生这种使用的每个翻译单元中;不需要诊断。”。这种“不需要诊断”的东西将允许实现做任何它想做的事情。
【解决方案2】:

我知道这个问题很老,但它引起了我的兴趣,我不得不尝试在运行时不填充动态容器的情况下:

#ifndef _FIBONACCI_HPP
#define _FIBONACCI_HPP


template <unsigned long N>
struct Fibonacci
{
    static const unsigned long long value = Fibonacci<N-1>::value + Fibonacci<N-2>::value;

    static unsigned long long get_value(unsigned long n)
    {
        switch (n) {
            case N:
                return value;
            default:
                return n < N    ? Fibonacci<N-1>::get_value(n)
                                : get_value(n-2) + get_value(n-1);
        }
    }
};

template <>
struct Fibonacci<0>
{
    static const unsigned long long value = 0;

    static unsigned long long get_value(unsigned long n)
    {
        return value;
    }
};

template <>
struct Fibonacci<1>
{
    static const unsigned long long value = 1;

    static unsigned long get_value(unsigned long n)
    {
        return value;
    }
};

#endif

这似乎可行,并且当使用优化编译时(不确定您是否允许这样做),调用堆栈不会深入 - 当然对于值(参数)n 堆栈上有正常的运行时递归> N,其中 N 是模板实例化中使用的 TableSize。但是,一旦低于 TableSize,生成的代码将替换在编译时计算的常量,或者最坏的情况是通过跳过跳转表“计算”的值(在 gcc 中使用 -c -g -Wa,-adhlns=main. s 并检查了清单),与我认为您的显式 switch 语句会导致的结果相同。

当这样使用时:

int main()
{
    std::cout << "F" << 39 << " is " << Fibonacci<40>::get_value(39) << '\n';
    std::cout << "F" << 45 << " is " << Fibonacci<40>::get_value(45) << '\n';
}

在第一种情况下根本没有调用计算(在编译时计算的值),在第二种情况下调用堆栈深度最差:

fibtest.exe!Fibonacci<40>::get_value(unsigned long n=41)  Line 18 + 0xe bytes    C++
fibtest.exe!Fibonacci<40>::get_value(unsigned long n=42)  Line 18 + 0x2c bytes    C++
fibtest.exe!Fibonacci<40>::get_value(unsigned long n=43)  Line 18 + 0x2c bytes    C++
fibtest.exe!Fibonacci<40>::get_value(unsigned long n=45)  Line 18 + 0xe bytes    C++
fibtest.exe!main()  Line 9 + 0x7 bytes    C++
fibtest.exe!__tmainCRTStartup()  Line 597 + 0x17 bytes    C

即它递归,直到它在“表”中找到一个值。 (通过在调试器中逐行通过反汇编进行验证,也通过将测试整数替换为随机数

递归部分也可以用线性迭代解代替:

static unsigned long long get_value(unsigned long n)
{
    switch (n) {
        case N:
            return value;    
        default:
            if (n < N) {
                return Fibonacci<N-1>::get_value(n);
            } else {
                // n > N
                unsigned long long i = Fibonacci<N-1>::value, j = value, t;
                for (unsigned long k = N; k < n; k++) {
                    t = i + j;
                    i = j;
                    j = t;
                }
                return j;
            }
    }
}

【讨论】:

  • 非常好,感谢您的回答。实际上处理了我最初的问题:“也许是 switch 语句的模板元编程版本,它会生成一个特定数量的 switch 语句?”实际上就是这样。
  • 看起来不错,但我个人不喜欢的事实是,您在“默认”情况下隐藏了第二个斐波那契计算实现。对于一般情况(也许使用 c++0x constexpr 函数来定义共享功能),这种解决方案是否可行?
  • 另一种明显的方法是定义接口,这样用户就不能使用 n>N 调用 Fibonacci::get_value(n)(抛出异常,断言 ...)。对于实际使用,这通常是合理的,并且功能重复性会立即消失。
  • 是的,那是我的第一次剪辑,代码最终变得更加紧凑。但我想尝试解决原始问题中的“如果值在表之外,它会返回零而不是计算它”(您将返回零替换为异常)。
【解决方案3】:

如果您有支持可变参数模板(C++0x 标准)的 C++ 编译器,您可以在编译时将 fibonacii 序列保存在一个元组中。在运行时,您可以通过索引访问该元组中的任何元素。

#include <tuple>   
#include <iostream>

template<int N>
struct Fib
{
    enum { value = Fib<N-1>::value + Fib<N-2>::value };
};

template<>
struct Fib<1>
{
    enum { value = 1 };
};

template<>
struct Fib<0>
{
    enum { value = 0 };
};

// ----------------------
template<int N, typename Tuple, typename ... Types>
struct make_fibtuple_impl;

template<int N, typename ... Types>
struct make_fibtuple_impl<N, std::tuple<Types...> >
{
    typedef typename make_fibtuple_impl<N-1, std::tuple<Fib<N>, Types... > >::type type;
};

template<typename ... Types>
struct make_fibtuple_impl<0, std::tuple<Types...> >
{
    typedef std::tuple<Fib<0>, Types... > type;
};

template<int N>
struct make_fibtuple : make_fibtuple_impl<N, std::tuple<> >
{};

int main()
{
   auto tup = typename make_fibtuple<25>::type();
   std::cout << std::get<20>(tup).value;  
   std::cout << std::endl; 

   return 0;
}

【讨论】:

  • “20”能否像 OP 要求的那样来自用户输入?
  • @KubaWyrostek 是的,如果用户可以在源文件中输入并将其提供给编译器。
【解决方案4】:

使用 C++11:您可以创建一个 std::array 和一个简单的 getter:https://ideone.com/F0b4D3

namespace detail
{

template <std::size_t N>
struct Fibo :
    std::integral_constant<size_t, Fibo<N - 1>::value + Fibo<N - 2>::value>
{
    static_assert(Fibo<N - 1>::value + Fibo<N - 2>::value >= Fibo<N - 1>::value,
                  "overflow");
};

template <> struct Fibo<0u> : std::integral_constant<size_t, 0u> {};
template <> struct Fibo<1u> : std::integral_constant<size_t, 1u> {};

template <std::size_t ... Is>
constexpr std::size_t fibo(std::size_t n, index_sequence<Is...>)
{
    return const_cast<const std::array<std::size_t, sizeof...(Is)>&&>(
        std::array<std::size_t, sizeof...(Is)>{{Fibo<Is>::value...}})[n];
}

template <std::size_t N>
constexpr std::size_t fibo(std::size_t n)
{
    return n < N ?
        fibo(n, make_index_sequence<N>()) :
        throw std::runtime_error("out of bound");
}
} // namespace detail

constexpr std::size_t fibo(std::size_t n)
{
    // 48u is the highest
    return detail::fibo<48u>(n);
}

在 C++14 中,你可以简化一些函数:

template <std::size_t ... Is>
constexpr std::size_t fibo(std::size_t n, index_sequence<Is...>)
{
    constexpr std::array<std::size_t, sizeof...(Is)> fibos{{Fibo<Is>::value...}};
    return fibos[n];
}

【讨论】:

    【解决方案5】:

    我的想法是递归地将斐波那契序列保存在可变参数模板中,然后将其转换为数组。所有这些都是在编译时完成的。 例如,当 n = 5 时,我们有:

    F<5>::array
    = F<4, 0>::array
    = F<3, 0, 1>::array
    = F<2, 0, 1, 1>::array
    = F<1, 0, 1, 1, 2>::array
    = F<0, 0, 1, 1, 2, 3>::array
    = { 0, 1, 1, 2, 3 }
    

    然后我们可以在运行时索引数组。

    我的 C++14 实现:

    #include <cstdint>
    #include <array>
    #include <iostream>
    
    
    template<uint64_t n>
    struct Helper { static constexpr uint64_t value = Helper<n - 1>::value + Helper<n - 2>::value; };
    
    template<>
    struct Helper<0> { static constexpr uint64_t value = 0; };
    
    template<>
    struct Helper<1> { static constexpr uint64_t value = 1; };
    
    
    template<u_int64_t x>
    class Fib {
    private:
        template<u_int64_t n, u_int64_t...rest>
        struct Get {
            static constexpr std::array<u_int64_t, n + sizeof...(rest)> value = Get<n - 1, rest..., Helper<sizeof...(rest)>::value>::value;
        };
    
        template<u_int64_t...rest>
        struct Get<0, rest...> {
            static constexpr std::array<u_int64_t, sizeof...(rest)> value{rest...};
        };
    public:
        static constexpr std::array<u_int64_t, x> sequence = Get<x>::value;
    };
    
    template<u_int64_t x>
    constexpr std::array<u_int64_t, x> Fib<x>::sequence;
    
    
    int main() {
        for (int i = 0; i < 45; i++) std::cout << "F" << i << " = " << Fib<45>::sequence[i] << std::endl;
    }
    

    【讨论】:

      【解决方案6】:

      C(大部分是 C++)的基本优势之一是您无需为不需要的东西付费。

      查找表的自动生成并不是编译器需要为您做的事情。即使您需要该功能,也不是其他所有人都需要。

      如果您想要一个查找表,请编写一个程序来制作一个。然后在您的程序中使用该数据。

      如果您希望在运行时计算值,请不要使用模板元程序,只需使用常规程序来计算值。

      【讨论】:

        【解决方案7】:

        您可以使用预处理器元编程技术生成开关或静态数组。 如果复杂度不超过该方法的限制,并且您不希望通过生成代码或数据的额外步骤来扩展您的工具链,那么这是一个不错的决定。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2021-04-25
          • 2014-04-11
          • 1970-01-01
          • 2012-11-03
          • 2013-03-26
          相关资源
          最近更新 更多