フリーキーズ | 独学プログラミング

C++のconstexprとconstevalを学ぶ!コンパイル時計算でプログラムを高速化する方法

リンドくん

リンドくん

先生、C++の「constexpr」って何ですか?よく見かけるんですけど、何をするためのものなんでしょうか?

たなべ

たなべ

constexprはコンパイル時に計算を済ませておくための機能なんだ。
つまり、プログラムが実行される前に計算結果が決まってしまうから、実行時のパフォーマンスが格段に向上するんだよ。

C++を学んでいると、必ず出会うのが「constexpr」というキーワードです。
また、C++20からは「consteval」という新しいキーワードも追加されました。これらは一体何をするものなのでしょうか?

実は、これらの機能を理解することで、プログラムの実行速度を大幅に向上させることができるんです。
特に、計算量の多い処理や、値が決まっている計算については、実行時ではなくコンパイル時に処理を完了させることで、驚くほど高速なプログラムを作ることができます。

この記事では、C++初心者の方でも理解できるよう、constexprとconstevalの基本概念から実践的な使い方まで、段階的に解説していきます。

プログラミング学習でお悩みの方へ

HackATAは、エンジニアを目指す方のためのプログラミング学習コーチングサービスです。 経験豊富な現役エンジニアがあなたの学習をサポートします。

✓ 質問し放題

✓ β版公開中(2025年内の特別割引)

HackATAの詳細を見る

constexprとは?コンパイル時計算の基本概念

リンドくん

リンドくん

「コンパイル時に計算する」って、具体的にはどういうことなんですか?

たなべ

たなべ

例えば、円の面積を計算する関数があるとするよね。
通常なら実行時に計算するんだけど、constexprを使うとプログラムをビルドする段階で計算が終わっちゃうんだ。つまり、実行時には既に答えが用意されているということなんだよ。

constexprの基本的な仕組み

constexprは「constant expression(定数式)」の略で、コンパイル時に値が決定できる式や関数を表すキーワードです。

通常のプログラムでは、計算はプログラムが実行されるときに行われます。
しかし、constexprを使うことで、その計算をコンパイル時(プログラムをビルドするとき)に済ませておくことができるのです。

これにより得られるメリットは以下の通りです。

  • 実行時のパフォーマンス向上 - 計算済みの値を使用するため処理が高速
  • メモリ使用量の削減 - 計算結果が定数として埋め込まれる
  • エラーの早期発見 - コンパイル時に計算エラーが検出される

簡単な例で理解する

まずは、constexprを使わない通常の関数と、constexprを使った関数を比較してみましょう。

#include <iostream>

// 通常の関数
int square_normal(int n)
{
    return n * n;
}

// constexpr関数
constexpr int square_constexpr(int n)
{
    return n * n;
}

int main()
{
    // 通常の関数:実行時に計算される
    int result1 = square_normal(5);
    
    // constexpr関数:コンパイル時に計算される(引数が定数の場合)
    constexpr int result2 = square_constexpr(5);
    
    std::cout << "通常の関数: " << result1 << std::endl;
    std::cout << "constexpr関数: " << result2 << std::endl;
    
    return 0;
}

この例では、square_constexpr(5)コンパイル時に25という値が計算され、実行時には既に答えが用意されている状態になります。

constexpr関数の書き方と使い方

constexpr関数の基本ルール

constexpr関数を書く際は、いくつかの制約があります。
これらは、コンパイル時に安全に実行できることを保証するためのものです。

主な制約

  • 関数内で使用できるのは、コンパイル時に値が確定する操作のみ
  • 動的メモリ確保(new、malloc等)は使用不可
  • 仮想関数は使用不可
  • 例外の投げ方に制限がある(C++20以降は緩和)

constexpr変数の活用

関数だけでなく、変数にもconstexprを使用できます。

#include <iostream>
#include <array>

constexpr double PI = 3.14159265359;
constexpr double circle_area(double radius)
{
    return PI * radius * radius;
}

int main()
{
    // 円の面積をコンパイル時に計算
    constexpr double area1 = circle_area(5.0);   // 半径5
    constexpr double area2 = circle_area(10.0);  // 半径10
    
    std::cout << "半径5の円の面積: " << area1 << std::endl;
    std::cout << "半径10の円の面積: " << area2 << std::endl;
    
    return 0;
}

C++20の新機能 - consteval

リンドくん

リンドくん

C++20で追加された「consteval」っていうのは、constexprとどう違うんですか?

たなべ

たなべ

constevalは強制的にコンパイル時実行を行うキーワードなんだ。
constexprは「できればコンパイル時に」だけど、constevalは「絶対にコンパイル時に」という感じだね。より厳密な制御ができるんだよ。

constevalとconstexprの違い

constexpr

  • コンパイル時に計算できる場合はコンパイル時に実行
  • 実行時にも呼び出すことが可能

consteval

  • 必ずコンパイル時に実行される
  • 実行時に呼び出すことは不可能
  • 「immediate function(即時関数)」とも呼ばれる

constevalの実践例

#include <iostream>

// consteval関数:必ずコンパイル時に実行される
consteval int power_compile_time(int base, int exponent)
{
    int result = 1;
    for (int i = 0; i < exponent; ++i) {
        result *= base;
    }
    return result;
}

// constexpr関数:コンパイル時または実行時に実行される
constexpr int power_flexible(int base, int exponent)
{
    int result = 1;
    for (int i = 0; i < exponent; ++i) {
        result *= base;
    }
    return result;
}

int main()
{
    // consteval:必ずコンパイル時に計算される
    constexpr auto result1 = power_compile_time(2, 10); // 1024
    
    // constexpr:定数引数ならコンパイル時、変数引数なら実行時
    constexpr auto result2 = power_flexible(3, 4);      // 81(コンパイル時)
    
    int runtime_value = 5;
    auto result3 = power_flexible(2, runtime_value);    // 32(実行時)
    
    // 以下はコンパイルエラー!constevalは実行時に呼び出せない
    // auto result4 = power_compile_time(2, runtime_value);
    
    std::cout << "2^10 = " << result1 << std::endl;
    std::cout << "3^4 = " << result2 << std::endl;
    std::cout << "2^5 = " << result3 << std::endl;
    
    return 0;
}

constevalが役立つ場面

constevalは、以下のような場面で特に威力を発揮します。

コンパイル時の設定値検証

#include <iostream>
#include <stdexcept>

consteval int validate_port(int port)
{
    if (port < 1 || port > 65535) {
        throw std::invalid_argument("ポート番号は1-65535の範囲で指定してください");
    }
    return port;
}

consteval const char* validate_protocol(const char* protocol)
{
    // 簡単な検証例
    if (protocol[0] == '\0') {
        throw std::invalid_argument("プロトコルが空です");
    }
    return protocol;
}

int main()
{
    // これらの値はコンパイル時に検証される
    constexpr int server_port = validate_port(8080);
    constexpr const char* protocol = validate_protocol("HTTP");
    
    // 以下はコンパイルエラーになる
    // constexpr int invalid_port = validate_port(70000); // 範囲外
    
    std::cout << "サーバーポート: " << server_port << std::endl;
    std::cout << "プロトコル: " << protocol << std::endl;
    
    return 0;
}

実践的な活用例とパフォーマンス比較

ゲーム開発での活用例

ゲーム開発では、しばしば大量の計算が必要になります。
constexprとconstevalを活用することで、これらの計算をコンパイル時に済ませることができます。

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

// 三角関数のテーブルをコンパイル時に生成
constexpr double PI = 3.14159265358979323846;

consteval double sin_approx(double x)
{
    // テイラー級数による近似(簡易版)
    double result = x;
    double term = x;
    for (int i = 1; i < 10; ++i) {
        term *= -x * x / ((2 * i) * (2 * i + 1));
        result += term;
    }
    return result;
}

// コンパイル時にsinテーブルを生成
template<size_t N>
consteval std::array<double, N> generate_sin_table()
{
    std::array<double, N> table{};
    for (size_t i = 0; i < N; ++i) {
        double angle = (2.0 * PI * i) / N;
        table[i] = sin_approx(angle);
    }
    return table;
}

int main()
{
    // 360度分のsinテーブル(コンパイル時に生成)
    constexpr auto sin_table = generate_sin_table<360>();
    
    // 使用例:45度のsin値
    constexpr size_t angle_45 = 45;
    std::cout << "sin(45°) ≈ " << sin_table[angle_45] << std::endl;
    
    // 90度のsin値
    constexpr size_t angle_90 = 90;
    std::cout << "sin(90°) ≈ " << sin_table[angle_90] << std::endl;
    
    return 0;
}

パフォーマンス比較の実例

constexprの効果を実際に測定してみましょう。

#include <iostream>
#include <chrono>

// 通常の関数
long long fibonacci_normal(int n)
{
    if (n <= 1) return n;
    return fibonacci_normal(n - 1) + fibonacci_normal(n - 2);
}

// constexpr関数
constexpr long long fibonacci_constexpr(int n)
{
    if (n <= 1) return n;
    return fibonacci_constexpr(n - 1) + fibonacci_constexpr(n - 2);
}

int main()
{
    const int N = 35;
    
    // 通常の関数の実行時間測定
    auto start = std::chrono::high_resolution_clock::now();
    auto result1 = fibonacci_normal(N);
    auto end = std::chrono::high_resolution_clock::now();
    auto duration1 = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
    
    // constexpr関数(コンパイル時計算)の実行時間測定
    start = std::chrono::high_resolution_clock::now();
    constexpr auto result2 = fibonacci_constexpr(N); // コンパイル時に計算済み
    end = std::chrono::high_resolution_clock::now();
    auto duration2 = std::chrono::duration_cast<std::chrono::nanoseconds>(end - start);
    
    std::cout << "フィボナッチ数列の" << N << "番目: " << result1 << std::endl;
    std::cout << "通常の関数の実行時間: " << duration1.count() << " ms" << std::endl;
    std::cout << "constexpr関数の実行時間: " << duration2.count() << " ns" << std::endl;
    std::cout << "速度向上: 約" << duration1.count() * 1000000 / duration2.count() << "倍" << std::endl;
    
    return 0;
}

注意点とベストプラクティス

よくある間違いと対処法

1. 実行時の値でconstexpr関数を呼び出す

#include <iostream>

constexpr int square(int n)
{
    return n * n;
}

int main()
{
    int user_input;
    std::cout << "数値を入力してください: ";
    std::cin >> user_input;
    
    // これは実行時計算になる(問題なし)
    auto result = square(user_input);
    
    // これはコンパイルエラー!constexprには定数が必要
    // constexpr auto compile_time_result = square(user_input);
    
    std::cout << "結果: " << result << std::endl;
    
    return 0;
}

2. constexpr関数内での制限事項

#include <iostream>
#include <vector>

// これはコンパイルエラー!動的メモリ確保は不可
/*
constexpr std::vector<int> create_vector(int size)
{
    return std::vector<int>(size, 0);
}
*/

// 代わりにstd::arrayを使用
#include <array>

template<size_t N>
constexpr std::array<int, N> create_array()
{
    std::array<int, N> arr{};
    for (size_t i = 0; i < N; ++i) {
        arr[i] = static_cast<int>(i * i);
    }
    return arr;
}

int main()
{
    constexpr auto arr = create_array<10>();
    
    for (size_t i = 0; i < arr.size(); ++i) {
        std::cout << "arr[" << i << "] = " << arr[i] << std::endl;
    }
    
    return 0;
}

効果的な使い方のガイドライン

  1. 計算コストの高い定数値の生成

    • 数学定数の計算
    • ルックアップテーブルの生成
    • 設定値の事前計算
  2. 型安全性の向上

    • コンパイル時の値検証
    • 範囲チェック
    • 型変換の安全性確保
  3. template metaprogrammingとの組み合わせ

    • 型に基づく計算
    • コンパイル時の条件分岐
    • 静的な設定管理

まとめ

リンドくん

リンドくん

constexprとconstevalって、思っていた以上に強力な機能なんですね!
特にパフォーマンスの向上が目に見えて分かるのが面白いです。

たなべ

たなべ

そうなんだ!最初は「コンパイル時計算」って聞くと難しそうに感じるかもしれないけど、使ってみると「なぜ今まで使わなかったんだろう」って思うようになるよ。
特に、ゲーム開発やパフォーマンスが重要なアプリケーションでは必須のテクニックだね。

この記事では、C++のconstexprとconstevalについて、基本概念から実践的な活用法まで詳しく解説してきました。

重要なポイントをおさらいしましょう。

  • constexprは可能な場合にコンパイル時計算を行い、実行時のパフォーマンスを大幅に向上させる
  • constevalは強制的にコンパイル時実行を行い、より厳密な制御が可能
  • ゲーム開発や数値計算において、数十倍から数千倍のパフォーマンス向上が期待できる
  • 適切に使用することで、型安全性とパフォーマンスの両方を向上させることができる

これらの機能を理解することで、あなたのC++プログラムは次のレベルに到達するでしょう。
特に、AIや機械学習、ゲーム開発といった計算集約的な分野では、constexprとconstevalの活用が競争力の源泉となります。

最初は少し難しく感じるかもしれませんが、ぜひ実際にコードを書いて試してみてください。
コンパイル時計算の威力を体感することで、モダンC++の真の価値を理解できるはずです。

この記事をシェア

関連するコンテンツ