绕过模板参数整数限制

Bypass template argument integer limit

我正在尝试从模板元编程开始。我写了这样的简单结构:

template<int N>
class Fact {
  public:
    enum { result = N * Fact<N-1>::result };
};

template<>
class Fact<0> {
  public:
    enum { result = 1 };
};

当我尝试在 main 中调用它时,如:

int main() {
    Fact<5> f;
    std::cout << f.result;
}

它适用于较小的参数值。一旦我提供更大的值,比如 15,编译器就会报错:integer overflow in constant expression。截断似乎不是由编译器隐式完成的。如果我恢复到较小的值并且我得到要编译的代码,那么我仍然会收到警告:variable set but unused。为什么?

现在,我对这段代码进行了更多更改,例如:

template<int N>
class Fact {
  public:
    static long result;
};

template<>
class Fact<0> {
  public:
    static long result;
};

long Fact<0>::result = 1;
template<int N>
long Fact<N>::result = N * Fact<N-1>::result;

此时警告消失了,而且似乎也允许整数换行。我不明白为什么会这样。我试着写一个香草 class 像:

class X{
public:
    static int d;
    enum { r = 10000000000000000l };
};

int X::d = 50;

当我使用它时,我仍然收到 unused 警告,枚举也会编译。有人能帮我理解在编译阶段的什么时候,模板推导会发生吗?为什么我能够绕过第一种情况下进行的编译器检查?谢谢!

在您的第一个示例中,Fact<0>::result 的基础类型是 int。由于您的基本模板的 N 参数也是一个 int,因此 N * Fact<N-1>::result 的结果也是 int。这意味着在 N = 13 处,N * Fact<N-1>::result 的结果将溢出一个 4 字节的 int。有符号整数溢出是未定义的行为。指定的 enum 值是编译时常量,编译时常量表达式中的未定义行为是不允许的,因此会出现硬错误。

在您的第二个示例中,您不仅使用了 long,它的范围可能比 int 更大,而且 long Fact<N>::result = N * Fact<N-1>::result; 不是编译时常量表达式。这意味着任何潜在的未定义行为在编译时都不会被检测到。您的程序的行为仍然未定义。

在现代 C++ 中生成编译时阶乘的惯用方法是使用 constexpr 变量而不是未限定范围的 enums:

template<unsigned long N>
struct Fact {
    static constexpr unsigned long result = N * Fact<N-1>::result;
};

template<>
struct Fact<0> {
    static constexpr unsigned long result = 1;
};

constexpr函数:

constexpr unsigned long fact(unsigned long n)
{
    unsigned long ret = 1;
    for (unsigned long i = 1; i <= n; ++i) {
        ret *= i;
    }
    return ret;
}

为了在溢出时保持行为定义明确,我让 result 成员未签名。


如果您真的需要保持与旧编译器的兼容性,您可以将另一个成员添加到您的无范围 enum 中,该成员足够大以强制其基础类型为 unsigned long:

template<unsigned long N>
struct Fact {
    enum {
        result = N * Fact<N-1>::result,
        placeholder = ULONG_MAX
    };
};

template<>
struct Fact<0> {
    enum {
        result = 1,
        placeholder = ULONG_MAX
    };
};

请注意,我使用 ULONG_MAX 而不是 std::numeric_limits<unsigned long>::max(),因为后者只是 C++11 或更高版本中的编译时常量表达式,也是我能想到使用的唯一原因这种方法是为了保持与 C++11 之前的编译器的兼容性。

您可以使用类型 long long.

而不是 int 或 long
class Fact {
public:
    enum: long long { result = N * Fact<N - 1>::result };
};

template<>
class Fact<0> {
public:
    enum : long long { result = 1 };
};

但是连long long都可以溢出。在汇编程序中有一个溢出标志。但在 C++ 中你可以使用一个技巧,如果结果小于初始值,则在加法或乘法过程中发生溢出。