constexpr 解答ページ | Programming Place Plus 新C++編

トップページ新C++編constexpr

このページの概要 🔗

このページは、練習問題の解答例や解説のページです。



解答・解説 🔗

問題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";
}


constexpr関数はコンパイル時計算の文脈で呼び出すことができる関数です。しかし、そうでない文脈で呼び出すこともできます(本編解説)。つまり、コンパイル時に呼び出せるならそうするし、呼び出せないなら実行時に呼び出そうとします。

A、B、C、E の4箇所は、constexpr変数の初期化子として f を呼び出しています。constexpr変数は、その値がコンパイル時に計算されて、結果的に定数に評価される変数です(本編解説)。そのため、f の呼び出しをコンパイル時に行うことを要請されていることになります。問題はその要請に応えることが可能なのかどうかで、以下がポイントになります。

f が constexpr関数なのは、関数宣言に constexprキーワードが付加されていることで明らかですが、実引数のほうは検討が必要です。

A の箇所で渡している実引数はリテラルなので、コンパイル時にそのまま使えますから問題ありません。f はコンパイル時に呼び出されます。

B の箇所で渡している実引数は constexpr変数である x と y です。constexpr変数はコンパイル時に使用できますし、見えるところにある(コード上、手前で宣言されている)ので、f はコンパイル時に呼び出されます。

C の箇所で渡している実引数は constexpr変数である x と、そうではない変数 z です。x は問題ないですが、z が問題です。変数z の値が決定するのは実行時なので、コンパイル時に f に値を渡せません。したがって、f の呼び出しをコンパイル時には行えません。しかし、ans3 の側が constexpr変数なので、コンパイル時に結果が受け取れなければなりませんから、f の呼び出しを実行時まで遅らせることもできず、コンパイルエラーとなります。

D の箇所は、const変数の初期化子として f を使用しており、実引数は constexpr ではない変数 z と 1 です。変数z の値がコンパイル時に決定できないため、f の呼び出しは実行時になります。変数ans4 は constexpr変数ではないので、f の呼び出しが実行時に先送りとなっても問題ありません。

E の箇所で渡している実引数は const変数である ans4 と 1 です。ans5 は constexpr変数なので、コンパイル時に初期化できなければなりませんが、ans4 の値が決定できないため、コンパイルエラーになります。

もし変数ans4 の宣言が const int ans4{f(1, 1)}; のようになっていれば、コンパイラの判断によって、f(1, 1) の部分をコンパイル時に呼び出す可能性があります。そうなった場合、const int ans4{2}; としていることになり、整数型か列挙型の const修飾型の変数が定数式で初期化されている場合は、その変数を定数式として使用できるルールにより(「配列とポインタ」のページを参照)、f(ans4, 1)f(2, 1) とみなすことができ、E の箇所でもコンパイル時の呼び出しが可能になります。

問題2 (基本★★) 🔗

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


たとえば次のように実装できます。

#include <array>
#include <iostream>

template <typename T, std::size_t Size>
constexpr auto sum_elems(const std::array<T, Size>& array)
{
    T sum {0};
    for (int i = 0; i < Size; ++i) {
        sum += array[i];
    }
    return sum;
}

int main()
{
    constexpr std::array<int, 10> ary {0,1,2,3,4,5,6,7,8,9};
    constexpr auto sum = sum_elems(ary);
    std::cout << sum << "\n";
}

実行結果:

45

std::array には明示的なコンストラクタがないため、暗黙的に生成されるコンストラクタが constexprコンストラクタとして機能します(本編解説)。そのため std::array をコンパイル時に使用できます。

sum_elems関数テンプレートを実装することに難しいところはありませんが、範囲for文を使って実装しようとするとコンパイルエラーになるかもしれません。これはC++14 の std::array では、beginメンバ関数や endメンバ関数が constexpr関数になっていないためです。

#include <array>
#include <iostream>

template <typename T, std::size_t Size>
constexpr auto sum_elems(const std::array<T, Size>& array)
{
    T sum {0};
    for (const auto& e : array) {  // C++14 ではコンパイルエラー
        sum += e;
    }
    return sum;
}

int main()
{
    constexpr std::array<int, 10> ary {0,1,2,3,4,5,6,7,8,9};
    constexpr auto sum = sum_elems(ary);
    std::cout << sum << "\n";
}

実行結果:

45

C++17以降では、この実装でも正常に動作します。

問題3 (応用★★) 🔗

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


問題2で作った sum_elems関数テンプレートを利用します。時間の計測には「chrono」のページで紹介した方法を使います。

#include <array>
#include <chrono>
#include <iostream>

#define CONSTEXPR  constexpr

template <typename T, std::size_t Size>
CONSTEXPR auto sum_elems(const std::array<T, Size>& array)
{
    T sum{0};
    for (int i = 0; i < Size; ++i) {
        sum += array[i];
    }
    return sum;
}

int main()
{
    CONSTEXPR std::array<int, 10000> ary{0,1,2,3,4,5,6,7,8,9};

    {
        auto begin = std::chrono::steady_clock::now();

        for (int i = 0; i < 1000; ++i) {
            CONSTEXPR auto sum = sum_elems(ary);
            std::cout << sum << "\n";
        }

        auto end = std::chrono::steady_clock::now();
        auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(end - begin);
        std::cout << elapsed.count() << "milli sec.\n";
    }
}

要素の値は特に関係ないので初期化子は直していませんが、ary の要素数は増やしています。もっと増やしてもいいですが、コンパイラの限界でコンパイルエラーにされてしまうかもしれません。ここでは 10000個にしたうえで、sum_elems関数テンプレートを呼び出すところを 1000回繰り返すことで、差がはっきり分かるようにしています。

また、CONSTEXPRマクロを定義して、constexpr の効果をまとめて ON/OFF できるようにしました。CONSTEXPR が constexpr に置換される場合と、空に置換される場合とを比較します。

テスト環境では以下のような結果になりました。実行時の処理は削減されているようです。

実行結果(constexpr を使っていないとき):

45
(省略。45 が並ぶ)
272micro sec.

実行結果(constexpr を使ったとき):

45
(省略。45 が並ぶ)
79micro sec.


参考リンク 🔗



更新履歴 🔗




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