constexpr | Programming Place Plus 新C++編

トップページ新C++編

先頭へ戻る

このページの概要 🔗

このページでは、コンパイルを行っているときに計算を済ませるコンパイル時計算を実現する方法として重要な、constexpr について取り上げます。コンパイル時計算は、実行時に行う計算を減らすことで高速化を図ることができるほか、エラーの可能性を実行時に持ち込まなくて済むことで安全性を向上させる効果もあります。

このページの解説は C++14 をベースとしています

以下は目次です。要点だけをさっと確認したい方は、「まとめ」をご覧ください。



コンパイル時計算 🔗

テンプレートのインスタンス化と特殊化」のページの中で、階乗を求めるテンプレートが登場しました。練習問題では、テンプレートのインスタンス化がコンパイル時に行われることと、インスタンス化を再帰できることを利用して、コンパイル時📘に計算を行ってしまえることを取り上げました。

このように、コンパイル時に計算などの処理を行うことをコンパイル時計算 (compile-time computation) と呼びます。コンパイル時計算は、コンパイルにかかる時間が増加するデメリットがあるものの、プログラムの実行時におこなう計算が減るため、実行時の処理時間が削減され、高速なプログラムを実現する助けになります。また、処理の中で起こり得るエラーをコンパイル時に捕まえられる可能性があるので、コードの安全性にも寄与します。

ここまでのページで使ってきた constexpr変数も、初期化子のところでコンパイル時の計算を行っているといえます。このページでは constexpr変数について改めて取り上げた後、constexpr のほかの使い方を説明していきます。

constexpr変数 🔗

constexpr変数 (constexpr variable) は、値がコンパイル時に計算されて、定数に評価される変数です。次のように constexprキーワードを付加して定義します。静的データメンバや、変数テンプレートに対しても同様の方法で使用できます。

constexpr 型 識別子 初期化子;

constexpr変数が未初期化であることは許されません。

これまで、constexpr変数を定数と考えて使ってきましたが、本来的な意味はさきほど書いたとおり「コンパイル時に値が定数として評価される変数」です。コンパイルの途中で計算して値が決定するので、コンパイルの文脈に注目すればまだ変数であると考えられるため、constexpr”変数” という名称になっています。コンパイルが完了したあとのことを考えれば、constexpr変数はもはや絶対に変化しない値を持っているので定数と捉えていいというわけです。

constexpr変数と constな変数は異なるものです。const の本来的な意味は「値を変更できないこと」です。constな変数の値が決定されるタイミングは、一部の例外があるものの基本的には実行時であると考えられます(「配列とポインタ」のページを参照)。そのため、コンパイル時に値が決定される保証がある constexpr変数とは異なっています。

【C++20】初期化がコンパイル時に行われる保証を与えるために、constinitキーワードが追加されました[1]。constinitキーワードを付加して宣言された変数は、その初期化がコンパイル時に行われない場合にコンパイルエラーになります。これは const であること(値を変更できないこと)とは異なるので、constinit と const は別個に指定できます。一方で、constinit と constexpr は意味が重なっており、同時に指定することはできません。

とはいえ constexpr変数も、コンパイル時に決定された値はその後に変更することはできません。その点では const の性質を備えています。実際、constexpr変数は暗黙的に const です[2]

constexpr変数は定数式の中で使えます。そのため、配列の要素数の指定(「配列」のページを参照)や、コンパイル時アサート(「アサート」のページを参照)などでも使用できます。

#include <iostream>

int main()
{
    constexpr std::size_t size {100};
    int array[size] {};

    static_assert(sizeof(array) == sizeof(int) * size, "");
}

実行結果:

constexpr関数 🔗

constexprキーワードを、コンストラクタ以外の関数の宣言に付加した場合、その関数は constexpr関数 (constexpr function) になります。コンストラクタの場合は特別な扱いになるので、あとで取り上げます

constexpr 戻り値の型 関数名(仮引数の並び)
{
    // ...
}

constexpr関数は暗黙的にインライン関数になります[3]

静的でないメンバ関数を constexpr関数にした場合に、暗黙的に constメンバ関数になることはありません(C++11 では暗黙的に constメンバ関数になりましたが、仕様変更されています[4])。

(C++14時点での)constexpr関数には次の制約があります(まだ解説していない事項を含んでいます)[5]

複雑なので詳細は省きますが、constexpr関数の制約は C++11/14/17/20/23 のすべてでそれぞれ変更されています。基本的には、制約はどんどん緩くなっており、C++11 では異様なほど厳しかったですが、C++23 では制約の大半がなくなっています。[6]

リテラル型 (literal type) とは、コンパイル時に決定される値を保持できる型の総称です。詳細はAPPENDIX を参照してください。これまでに登場している型のほとんどはリテラル型として使えますが、クラス型などの例外があります。クラス型については、後述する constexprコンストラクタを定義することによってリテラル型として扱えるようになります。なお、void もリテラル型なので、仮引数や戻り値がない constexpr関数も許されます。

次のプログラムでは、べき乗を求める関数を constexpr関数で定義しています。

#include <iostream>

constexpr int pow(int base, int exp)
{
    int result {1};
    for (int i = 0; i < exp; ++i) {
        result *= base;
    }
    return result;
}

int main()
{
    constexpr int result {pow(2, 8)};
    std::cout << result << "\n";
}

実行結果:

256

pow関数の呼び出しは constexpr変数の初期化子のところにあります。constexpr変数の値はコンパイル時に決定されるため、pow関数もまたコンパイル時に呼び出されなければなりません。もし pow関数が constexpr関数でなければ、このプログラムはコンパイルできません。

また、constexpr関数に渡す実引数が定数式でなかったり、constexpr関数の本体が constexpr関数でない関数を呼び出していたりすると、その部分がコンパイル時に計算できませんから、コンパイル時に呼び出すことができません。

さきほどのプログラムでいったん constexpr変数に結果を受け取っているのは、pow関数の呼び出しを確実にコンパイル時計算の文脈に入れるためです。わざわざ変数を経由するほどのものでもないため、以下のように書き換えることを考えるかもしれません。

#include <iostream>

constexpr int pow(int base, int exp)
{
    int result {1};
    for (int i = 0; i < exp; ++i) {
        result *= base;
    }
    return result;
}

int main()
{
    std::cout << pow(2, 8) << "\n";
}

実行結果:

256

std::cout による出力が行われるのはもちろん実行時なので、コンパイル時計算の文脈ではありません。この場合でも、pow関数の実引数は定数だけなので、コンパイラの判断によって pow(2, 8) の部分をコンパイル時に実行し、256 に置き換える最適化が行われる可能性はあります。ただしその保証はありません。

ここで1つ1つ注意すべきなのは、結果的にこのプログラムの pow関数の呼び出しがコンパイル時に行われたとしても、実行時に行われることになったとしても、このプログラムはコンパイルできるという点です。constexpr関数はコンパイル時計算の文脈の中で呼び出せる関数ですが、実行時に呼び出すこともできるからです。

この性質は、結局いつ呼び出されているのか分かりづらいという欠点を持っているともいえますが、コンパイル時計算にも実行時計算にも有用な関数を、コンパイル時バージョンと実行時バージョンとに分けて定義しなくて済む利点もあります。

【C++20】constexprキーワードを consteval に置き換えることで、コンパイル時にだけ呼び出せる関数を定義できるようになりました。このような関数を即時関数 (immediate function) と呼びます。[7]

標準ライブラリの関数の中にも constexpr関数になっているものがあります。また、前にあげた constexpr関数の制約が、C++ の規格の進化とともに取り払われていることに合わせて、標準ライブラリの関数のいくらかが constexpr関数に変更されることがあります。

【C++17】constexprラムダが導入され、ラムダ式にも constexprキーワードを付加できるようになりました[8]。生成されるクロージャ型がリテラル型として扱えるようになり、operator() が constexpr関数になるため、constexpr関数の中から使うことができます。

constexprコンストラクタ 🔗

コンストラクタに対して constexprキーワードを付加した場合、constexprコンストラクタ (constexpr constructor) になります。

class クラス名 {
    constexpr クラス名(仮引数の並び);
};

また、コンパイラが暗黙的に生成したコンストラクタは、自動的に constexprコンストラクタになっています[9]

constexprコンストラクタは暗黙的にインライン関数になります。[3]

(C++14時点での)constexprコンストラクタには次の制約があります(まだ解説していない事項を含んでいます)[10]

constexprコンストラクタを持ったクラスは、(厳密にはほかの条件もありますが)リテラル型とみなせるようになります。そのため、コンパイル時計算の文脈の中でも使用できます。

リテラル型とみなされる条件については APPENDIX を参照してください。

たとえば、金額を表す Moneyクラスのようなものは、コンパイル時計算の文脈でも使えると便利でしょう。

#include <iostream>

class Money {
public:
    constexpr explicit Money(int amount) : m_amount{amount}
    {}

    constexpr Money(const Money& money) : m_amount{money.m_amount}
    {}

    constexpr int get_amount() const
    {
        return m_amount;
    }

private:
    int  m_amount;
};

constexpr Money operator+(const Money& lhs, const Money& rhs)
{
    Money tmp {lhs.get_amount() + rhs.get_amount()};
    return tmp;
}

int main()
{
    constexpr Money m1 {10000};
    constexpr Money m2 {5000};
    constexpr Money m3 {m1 + m2};
    std::cout << m3.get_amount() << "\n";

    Money m4 {10000};
    Money m5 {5000};
    Money m6 {m4 + m5};
    std::cout << m6.get_amount() << "\n";
}

実行結果:

15000
15000

constexprコンストラクタが定義されているため、Moneyクラスをコンパイル時計算の中で使用できます。constexpr変数の型を Moneyクラスにできていますし、constexpr関数である operator+ を呼び出すこともできています。また、まったく同じことが実行時計算の文脈でも行えていることも分かります。


標準ライブラリのクラスやクラステンプレートにもコンパイル時計算で使えるものがあります。たとえば std::array は、要素の型がリテラル型であればコンパイル時計算の中でも使用できます。ただし C++14 の std::array は要素を変更しないようなメンバ関数にだけしか constexpr が付加されていないため、要素の書き換えはできません。

【C++17】ほかのメンバ関数にも constexpr が付加されました。

また、C++ の規格バージョンごとに constexpr の制約が緩くなっているため、途中の規格からコンパイル時計算で使えるようになる場合もあります。

【C++20】たとえば std::vector や std::basic_string は、C++20 で constexpr に対応されています。

まとめ 🔗


新C++編の【本編】の各ページには、末尾に練習問題があります。ページ内で学んだ知識を確認する簡単な問題から、これまでに学んだ知識を組み合わせなければならない問題、あるいは更なる自力での調査や模索が必要になるような高難易度な問題をいくつか掲載しています。


参考リンク 🔗


練習問題 🔗

問題の難易度について。

★は、すべての方が取り組める入門レベルの問題です。
★★は、自力でプログラミングができるようなるために、入門者の方であっても取り組んでほしい問題です。
★★★は、本格的にプログラマーを目指す人のための問題です。

問題1 (確認★)

次のコードには、関数f を呼び出しているコードが5箇所あります。それぞれについて、呼び出されるタイミングがコンパイル時なのか実行時なのか、またはコンパイルエラーとなるかを答えてください。

#include <iostream>

constexpr int f(int a, int b)
{
    return a + b;
}

int main()
{
    constexpr int x {100};
    constexpr int y {200};
    int z {300};

    constexpr int ans1 {f(100, 200)};  // A
    constexpr int ans2 {f(x, y)};      // B
    constexpr int ans3 {f(x, z)};      // C
    const int ans4 {f(z, 1)};          // D
    constexpr int ans5 {f(ans4, 1)};   // E

    std::cout << ans1 << "\n"
              << ans2 << "\n"
              << ans3 << "\n"
              << ans4 << "\n"
              << ans5 << "\n";
}

解答・解説

問題2 (基本★★)

任意の std::array を渡すと、その要素の値の合計値を返す関数テンプレートを、constexpr関数として作成してください。

解答・解説

問題3 (応用★★)

コンパイル時計算によって処理時間を減らせていることを、実測によって確認してください。

解答・解説


解答・解説ページの先頭



更新履歴 🔗




はてなブックマーク に保存 Pocket に保存 Facebook でシェア
X で ポストフォロー LINE で送る noteで書く
rss1.0 取得ボタン RSS 管理者情報 プライバシーポリシー
先頭へ戻る