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

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

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

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

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]);
}

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

18

Решение

Начиная с 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

25

Другие решения

Я думаю, что этот способ более читабелен:

#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 ++ — ish, когда используете массив
  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, в противном случае оператор [] из 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 и возвратом по значению сборка идентична версии с шаблонами:

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.
8

Один пример:

#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;
4

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

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

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

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

4
По вопросам рекламы [email protected]