Современный C ++: инициализировать таблицы constexpr

Предположим, у меня есть класс X, функциональность которого требует множества постоянных значений таблицы, скажем, массива A[1024]. У меня есть повторяющаяся функция f, которая вычисляет свои значения, что-то вроде

A[x] = f(A[x - 1]);

Предположим, что A[0] - известная константа, поэтому остальная часть массива тоже постоянна. Как лучше всего рассчитать эти значения заранее, используя возможности современного C ++, и без сохранения файла с жестко закодированными значениями этого массива? Моим обходным путем была статическая фиктивная переменная const:

const bool X::dummy = X::SetupTables();

bool X::SetupTables() {
    A[0] = 1;
    for (size_t i = 1; i <= A.size(); ++i)
        A[i] = f(A[i - 1]);
}

Но я считаю, что это не самый красивый путь. Примечание: я подчеркиваю, что массив довольно большой, и я хочу избежать уродства кода.


person Aleksandr Samarin    schedule 14.11.2017    source источник
comment
Почему вы помечаете как C ++ 11, так и C ++ 1z? Что вы имеете в виду красивый?   -  person Passer By    schedule 14.11.2017
comment
Возможный дубликат Программно создавать статические массивы во время компиляции в C ++ Посмотрите ответ Георга Фриче. Вам просто нужно определить рекурсивную структуру MetaFunc.   -  person freakish    schedule 14.11.2017
comment
@freakish похоже, но разве это не излишество для огромного массива ~ 1024? Я имею в виду глубину инстанцирования шаблона?   -  person Aleksandr Samarin    schedule 14.11.2017
comment
@AleksandrSamarin Максимальная глубина создания экземпляра шаблона может быть изменена в зависимости от вашего компилятора. Для gcc через флаг -ftemplate-depth=n. Обратите внимание, что стандарт C ++ 11 устанавливает значение по умолчанию 1024. Если таблица все еще слишком велика, вы можете предварительно сгенерировать всю таблицу, сохранить ее как файл заголовка C ++ и просто включить.   -  person freakish    schedule 14.11.2017
comment
Если вы столкнулись с проблемами глубины шаблона, вы можете переключиться с линейного шаблона посещения на двоичное дерево. Вы можете настроить это так, чтобы он по-прежнему посещал все элементы в одном и том же порядке, но имел глубину рекурсии только lg N вместо N.   -  person Ben Voigt    schedule 14.11.2017


Ответы (4)


Начиная с C ++ 14, for циклы разрешены в constexpr функциях. Более того, начиная с C ++ 17, std::array::operator[] тоже constexpr.

Итак, вы можете написать что-то вроде этого:

template<class T, size_t N, class F>
constexpr auto make_table(F func, T first)
{
    std::array<T, N> a {first};
    for (size_t i = 1; i < N; ++i)
    {
        a[i] = func(a[i - 1]);
    }
    return a;
}

Пример: https://godbolt.org/g/irrfr2.

person Bob__    schedule 14.11.2017

Думаю, такой способ более читабельный:

#include <array>

constexpr int f(int a) { return a + 1; }

constexpr void init(auto &A)
{
  A[0] = 1;
  for (int i = 1; i < A.size(); i++) {
    A[i] = f(A[i - 1]);
  }
}

int main() {
  std::array<int, 1024> A;
  A[0] = 1;
  init(A);
}

Мне нужно сделать заявление об отказе от ответственности, что для больших размеров массива не гарантируется создание массива за постоянное время. И принятый ответ с большей вероятностью сгенерирует полный массив во время расширения шаблона.

Но предлагаемый мной способ имеет ряд преимуществ:

  1. Вполне безопасно, что компилятор не съест всю вашу память и не сможет расширить шаблон.
  2. Скорость компиляции значительно выше
  3. Вы используете интерфейс C ++, когда используете массив
  4. Код в целом более читабельный

В конкретном примере, когда вам нужно только одно значение, вариант с шаблонами сгенерировал для меня только одно число, а вариант с std::array сгенерировал цикл.

Обновить

Благодаря Navin я нашел способ принудительно вычислить время компиляции массива.

Вы можете заставить его запускаться во время компиляции, если вы вернетесь по значению: std :: array A = init ();

Итак, с небольшими изменениями код выглядит следующим образом:

#include <array>

constexpr int f(int a) { return a + 1; }

constexpr auto init()
{
  // Need to initialize the array
  std::array<int, SIZE> A = {0};
  A[0] = 1;
  for (unsigned i = 1; i < A.size(); i++) {
    A[i] = f(A[i - 1]);
  }
  return A;
}

int main() {
  auto A = init();
  return A[SIZE - 1];
}

Чтобы это скомпилировать, нужна поддержка C ++ 17, иначе operator [] из std :: array не будет constexpr. Также обновляю замеры.

При выводе сборки

Как я уже упоминал ранее, вариант шаблона более лаконичен. Дополнительные сведения см. здесь.

В варианте шаблона, когда я просто выбираю последнее значение массива, вся сборка выглядит следующим образом:

main:
  mov eax, 1024
  ret

В то время как для варианта std :: array у меня есть цикл:

main:
        subq    $3984, %rsp
        movl    $1, %eax
.L2:
        leal    1(%rax), %edx
        movl    %edx, -120(%rsp,%rax,4)
        addq    $1, %rax
        cmpq    $1024, %rax
        jne     .L2
        movl    3972(%rsp), %eax
        addq    $3984, %rsp
        ret

С std :: array и return by value сборка идентична версии с шаблонами:

main:
  mov eax, 1024
  ret

О скорости компиляции

Я сравнил эти два варианта:

test2.cpp:

#include <utility>

constexpr int f(int a) { return a + 1; }

template<int... Idxs>
constexpr void init(int* A, std::integer_sequence<int, Idxs...>) {
    auto discard = {A[Idxs] = f(A[Idxs - 1])...};
    static_cast<void>(discard);
}

int main() {
    int A[SIZE];
    A[0] = 1;
    init(A + 1, std::make_integer_sequence<int, sizeof A / sizeof *A - 1>{});
}

test.cpp:

#include <array>

constexpr int f(int a) { return a + 1; }

constexpr void init(auto &A)
{
    A[0] = 1;
    for (int i = 1; i < A.size(); i++) {
        A[i] = f(A[i - 1]);
    }
}

int main() {
    std::array<int, SIZE> A;
    A[0] = 1;
    init(A);
}

Результат:

|  Size | Templates (s) | std::array (s) | by value |
|-------+---------------+----------------+----------|
|  1024 |          0.32 |           0.23 | 0.38s    |
|  2048 |          0.52 |           0.23 | 0.37s    |
|  4096 |          0.94 |           0.23 | 0.38s    |
|  8192 |          1.87 |           0.22 | 0.46s    |
| 16384 |          3.93 |           0.22 | 0.76s    |

Как я создал:

for SIZE in 1024 2048 4096 8192 16384
do
    echo $SIZE
    time g++ -DSIZE=$SIZE test2.cpp
    time g++ -DSIZE=$SIZE test.cpp
    time g++ -std=c++17 -DSIZE=$SIZE test3.cpp
done

А если включить оптимизацию, скорость кода с шаблоном еще хуже:

|  Size | Templates (s) | std::array (s) | by value |
|-------+---------------+----------------+----------|
|  1024 |          0.92 |           0.26 | 0.29s    |
|  2048 |          2.81 |           0.25 | 0.33s    |
|  4096 |         10.94 |           0.23 | 0.36s    |
|  8192 |         52.34 |           0.24 | 0.39s    |
| 16384 |        211.29 |           0.24 | 0.56s    |

Как я создал:

for SIZE in 1024 2048 4096 8192 16384
do
    echo $SIZE
    time g++ -O3 -march=native -DSIZE=$SIZE test2.cpp
    time g++ -O3 -march=native -DSIZE=$SIZE test.cpp
    time g++ -O3 -std=c++17 -march=native -DSIZE=$SIZE test3.cpp
done

Моя версия gcc:

$ g++ --version
g++ (Debian 7.2.0-1) 7.2.0
Copyright (C) 2017 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
person mcsim    schedule 14.11.2017
comment
Это хорошо, хорошо! В конкретном примере, когда вам нужно только одно значение, вариант с шаблонами сгенерировал для меня только одно число, а вариант с std :: array сгенерировал цикл. Не могли бы вы прояснить эту строку? - person Aleksandr Samarin; 14.11.2017
comment
Это просто цикл во время выполнения, поэтому время компиляции составляет O (1). - person Maxim Egorushkin; 14.11.2017
comment
Вы можете заставить его запускаться во время компиляции, если вы вернетесь по значению: std::array<int, 1024> A = init(); - person Navin; 14.11.2017

Один пример:

#include <utility>

constexpr int f(int a) { return a + 1; }

template<int... Idxs>
constexpr void init(int* A, std::integer_sequence<int, Idxs...>) {
    auto discard = {A[Idxs] = f(A[Idxs - 1])...};
    static_cast<void>(discard);
}

int main() {
    int A[1024];
    A[0] = 1;
    init(A + 1, std::make_integer_sequence<int, sizeof A / sizeof *A - 1>{});
}

Требуется -ftemplate-depth=1026 g++ переключатель командной строки.


Пример того, как сделать его статическим членом:

struct B
{
    int A[1024];

    B() {
        A[0] = 1;
        init(A + 1, std::make_integer_sequence<int, sizeof A / sizeof *A - 1>{});
    };
};

struct C
{
    static B const b;
};

B const C::b;
person Maxim Egorushkin    schedule 14.11.2017
comment
{A[Idxs] = f(A[Idxs - 1])...} Я не знал этого волшебного трюка! - person YSC; 14.11.2017
comment
И я думал, что выражение fold работает только с определенными бинарными операторами. C ++ 17 ура! - person YSC; 14.11.2017
comment
@YSC Это не 17-кратное выражение C ++, а расширение пакета значений шаблона C ++ 11. - person Maxim Egorushkin; 14.11.2017
comment
Богоявление! Хороший ответ. Однако для этого требуется -std = c ++ 1z. -std = c ++ 14 недостаточно. - person YSC; 14.11.2017
comment
Мне нравится его компактность, но у меня есть один вопрос: если A является статическим членом какого-то класса, где мне поместить init функцию? - person Aleksandr Samarin; 14.11.2017
comment
@YSC Я компилирую его с помощью -std=c++14. std::integer_sequence - это функция C ++ 14, хотя ее можно перенести на C ++ 11. - person Maxim Egorushkin; 14.11.2017
comment
@YSC size_tstd::size_t! - person Konrad Rudolph; 14.11.2017
comment
В чем смысл C? Мне кажется, что B может быть статической переменной с ограниченным пространством имен. - person Matthieu M.; 14.11.2017
comment
@MatthieuM. Точка C - это демонстрация варианта использования, запрошенного OP. - person Maxim Egorushkin; 14.11.2017
comment
@KonradRudolph Ho! По меньшей мере! Хорошо, что реализация GCC на SL больше не загрязняет глобальное пространство имен! - person YSC; 15.11.2017

просто для удовольствия, компактный однострочный С ++ 17 может быть (требуется std :: array A или какой-либо другой непрерывный по памяти кортеж):

std::apply( [](auto, auto&... x){ ( ( x = f((&x)[-1]) ), ... ); }, A );

обратите внимание, что это также можно использовать в функции constexpr.

Тем не менее, начиная с C ++ 14 мы можем использовать циклы в функциях constexpr, поэтому мы можем написать функцию constexpr, возвращающую std :: array напрямую, записанную (почти) обычным способом.

person Massimiliano Janes    schedule 14.11.2017