web-dev-qa-db-ja.com

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

しかし、私はそれが最も美しい道ではないと信じています。注:私は配列がかなり大きいことを強調し、コードの怪物を避けたいと思います。

20

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

27
Bob__

私はこの方法がより読みやすいと思います:

#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. コードは一般的に読みやすくなります

値が1つだけ必要な特定の例では、テンプレート付きのバリアントは1つの数値のみを生成しましたが、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のoperator []は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つのバリアントを比較しました:

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
mcsim

一例:

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

ちょうど楽しみのために、c ++ 17コンパクトなワンライナーはかもしれません(std :: array A、またはいくつかの他のmemory-contiguousタプルのようなが必要です):

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

これはconstexpr関数でも使用できることに注意してください。

つまり、c ++ 14以降では、constexpr関数でループを使用できるため、std :: arrayを返すconstexpr関数を直接記述し、(ほぼ)通常の方法で記述できます。

4