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

但我相信,这不是最美丽的方式。 注意:我强调数组相当大,我想避免代码的怪物。

自 C++14 起,constexpr函数中允许for循环。此外,自 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

我认为这种方式更具可读性:

#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. 使用
  4. 数组时会使用C++式接口
  5. 代码通常更具可读性

在一个特定示例中,当您只需要一个值时,带有模板的变体只为我生成了一个数字,而带有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

关于编译速度

我比较了这两种变体:

测试2.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>{});
}

测试.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.

一个例子:

#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=1026g++命令行开关。


如何使其成为静态成员的示例:

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;

只是为了好玩,一个 C++17 紧凑的单行代码可能是(需要一个 std::array A,或其他一些类似内存连续元组):

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

请注意,这也可以在 constexpr 函数中使用。

也就是说,从 c++14 开始,我们可以在 constexpr 函数中使用循环,因此我们可以编写一个 constexpr 函数,直接返回 std::array,(几乎)以通常的方式编写。

最新更新