Обход шаблона аргумента целочисленного ограничения - PullRequest
0 голосов
/ 23 апреля 2020

Я пытаюсь начать с шаблонного метапрограммирования. Я написал простую конструкцию, подобную этой:

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

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

Когда я попытался вызвать ее в основном, как:

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 X{
public:
    static int d;
    enum { r = 10000000000000000l };
};

int X::d = 50;

И когда я использовал его, я все равно получил предупреждение unused, и enum также компилируется. Может ли кто-нибудь помочь мне понять, в какой момент на этапе компиляции происходит вывод шаблона? Почему я могу обойти проверки компилятора, которые были в первом случае? Спасибо!

Ответы [ 2 ]

1 голос
/ 23 апреля 2020

В вашем первом примере базовый тип 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 способ сделать факториал времени компиляции в современном C ++ - использовать constexpr переменные, а не unscoped enum s:

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 или более поздних версиях, и единственная причина, по которой я могу использовать этот подход, заключается в поддержании совместимости с pre -C ++ 11 компиляторов.

0 голосов
/ 23 апреля 2020

Вместо int или long вы можете использовать тип long 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 ++ вы можете использовать хитрость, если результат меньше исходного значения, то при сложении или умножении произошло переполнение.

...