C++ TMP (テンプレートメタプログラミング) シリーズ
| Part1 constexpr | Part2 Concepts | Part3 Variadic | Part4 型リスト | Part5 正規表現 |
|---|---|---|---|---|
| 👈 Now |
はじめに
C++11で導入されたconstexpr。最初は「定数式で使える関数」程度だった。
でもC++14、C++17、C++20と進化して、今やコンパイル時に何でもできるレベルに。
このシリーズでは、Modern C++のテンプレートメタプログラミングを体系的に解説する。
constexprの進化
| バージョン | できること |
|---|---|
| C++11 | 1行のreturn文のみ |
| C++14 | ローカル変数、ループ、分岐 |
| C++17 | if constexpr、constexpr lambda |
| C++20 | 仮想関数、try-catch、動的メモリ |
C++11 constexpr
最初のconstexprは制約が厳しかった。
// C++11: return文1つだけ
constexpr int factorial_11(int n) {
return n <= 1 ? 1 : n * factorial_11(n - 1);
}
// OK
constexpr int fact5 = factorial_11(5); // 120
// NG: ローカル変数が使えない
// constexpr int sum_11(int n) {
// int result = 0; // エラー!
// for (int i = 1; i <= n; ++i) {
// result += i;
// }
// return result;
// }
三項演算子と再帰で頑張るしかなかった。
C++14 constexpr
ローカル変数とループが使えるようになった!
// C++14: ループが使える!
constexpr int factorial_14(int n) {
int result = 1;
for (int i = 2; i <= n; ++i) {
result *= i;
}
return result;
}
constexpr int sum_to_n(int n) {
int result = 0;
for (int i = 1; i <= n; ++i) {
result += i;
}
return result;
}
constexpr int fact10 = factorial_14(10);
constexpr int sum100 = sum_to_n(100); // 5050
制限
ただし、まだ以下は使えない:
- 動的メモリ確保
- 仮想関数
- try-catch
C++17 constexpr
if constexpr
コンパイル時に分岐を選択できる。
template<typename T>
constexpr auto process(T value) {
if constexpr (std::is_integral_v<T>) {
// 整数型の場合のみコンパイルされる
return value * 2;
} else if constexpr (std::is_floating_point_v<T>) {
// 浮動小数点型の場合のみコンパイルされる
return value + 0.5;
} else {
// それ以外
return value;
}
}
constexpr auto result_int = process(10); // 20
constexpr auto result_float = process(3.14); // 3.64
通常のifとの違い
template<typename T>
void print_all(T value) {
// 通常のif: 両方のブランチがコンパイルされる
if (std::is_integral_v<T>) {
std::cout << value * 2;
} else {
std::cout << value.to_string(); // 整数型でもコンパイルエラー!
}
}
template<typename T>
void print_all_constexpr(T value) {
// if constexpr: 選ばれたブランチのみコンパイル
if constexpr (std::is_integral_v<T>) {
std::cout << value * 2;
} else {
std::cout << value.to_string(); // 整数型ではコンパイルされない
}
}
constexpr lambda
// C++17からラムダもconstexprにできる
constexpr auto square = [](int x) { return x * x; };
constexpr int result = square(5); // 25
// 明示的なconstexpr指定も可能
constexpr auto cube = [](int x) constexpr { return x * x * x; };
C++20 constexpr
大幅な拡張!
constexpr仮想関数
struct Base {
constexpr virtual int value() const { return 1; }
constexpr virtual ~Base() = default;
};
struct Derived : Base {
constexpr int value() const override { return 2; }
};
constexpr int get_value() {
Derived d;
Base& b = d;
return b.value(); // 仮想関数呼び出し
}
constexpr int v = get_value(); // 2
constexpr try-catch
constexpr int safe_divide(int a, int b) {
try {
if (b == 0) {
throw "Division by zero"; // コンパイル時例外
}
return a / b;
} catch (...) {
return 0;
}
}
// ただし、実際に例外が投げられるとコンパイルエラー
constexpr int result = safe_divide(10, 2); // OK: 5
// constexpr int error = safe_divide(10, 0); // エラー!
constexpr動的メモリ
constexpr int sum_vector() {
std::vector<int> vec; // コンパイル時にvector!
for (int i = 1; i <= 10; ++i) {
vec.push_back(i);
}
int sum = 0;
for (int v : vec) {
sum += v;
}
return sum;
}
constexpr int result = sum_vector(); // 55
制約: コンパイル時に確保したメモリは、コンパイル時に解放しなければならない。
// NG: メモリがコンパイル時に残る
constexpr auto* get_ptr() {
return new int(42); // コンパイルエラー!
}
// OK: メモリを解放して値を返す
constexpr int get_value() {
auto* p = new int(42);
int value = *p;
delete p;
return value;
}
constexpr std::string
constexpr std::string make_greeting(std::string_view name) {
std::string result = "Hello, ";
result += name;
result += "!";
return result;
}
// C++20: std::stringがconstexprに
constexpr auto greeting = make_greeting("World"); // "Hello, World!"
consteval: 必ずコンパイル時評価
constexprは実行時評価も許容する。
constevalはコンパイル時のみ。
constexpr int square(int x) {
return x * x;
}
consteval int square_compile_time(int x) {
return x * x;
}
int main() {
int x = 5;
// constexpr: 実行時もOK
int a = square(x); // 実行時評価
// consteval: コンパイル時のみ
// int b = square_compile_time(x); // エラー!実行時変数は不可
constexpr int c = square_compile_time(5); // OK
}
使い分け
| 種類 | 用途 |
|---|---|
| constexpr | コンパイル時/実行時どちらでも |
| consteval | コンパイル時専用(より強い保証) |
| constinit | 静的初期化の保証 |
constinit: 静的初期化の保証
グローバル変数の初期化順序問題を解決。
// 問題: 初期化順序が不定
struct Config {
static int value;
};
int Config::value = compute_value(); // いつ呼ばれる?
// 解決: constinit
constinit int global_value = 42; // 必ず静的初期化
// constexprである必要はないが、静的初期化可能でなければならない
constinit thread_local int tls_value = 0;
実践例1: コンパイル時CRC32
constexpr uint32_t crc32_table[256] = []() consteval {
std::array<uint32_t, 256> table{};
for (uint32_t i = 0; i < 256; ++i) {
uint32_t crc = i;
for (int j = 0; j < 8; ++j) {
crc = (crc >> 1) ^ (0xEDB88320 & (-(crc & 1)));
}
table[i] = crc;
}
return table;
}();
constexpr uint32_t crc32(std::string_view data) {
uint32_t crc = 0xFFFFFFFF;
for (char c : data) {
crc = crc32_table[(crc ^ static_cast<uint8_t>(c)) & 0xFF] ^ (crc >> 8);
}
return crc ^ 0xFFFFFFFF;
}
// コンパイル時にハッシュ計算
constexpr uint32_t hash = crc32("Hello, World!");
実践例2: コンパイル時ソート
template<typename T, size_t N>
constexpr std::array<T, N> sorted(std::array<T, N> arr) {
// バブルソート(コンパイル時間は気にしない)
for (size_t i = 0; i < N - 1; ++i) {
for (size_t j = 0; j < N - i - 1; ++j) {
if (arr[j] > arr[j + 1]) {
auto temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
return arr;
}
constexpr std::array<int, 5> unsorted = {5, 2, 8, 1, 9};
constexpr std::array<int, 5> sorted_arr = sorted(unsorted);
// sorted_arr = {1, 2, 5, 8, 9}
実践例3: コンパイル時JSON検証
enum class JsonError {
None,
UnexpectedChar,
UnterminatedString,
// ...
};
constexpr JsonError validate_json(std::string_view json) {
size_t pos = 0;
auto skip_whitespace = [&]() {
while (pos < json.size() &&
(json[pos] == ' ' || json[pos] == '\n' ||
json[pos] == '\t' || json[pos] == '\r')) {
pos++;
}
};
// 簡略化した検証ロジック
skip_whitespace();
if (pos >= json.size()) {
return JsonError::UnexpectedChar;
}
if (json[pos] == '{' || json[pos] == '[') {
// オブジェクトまたは配列
return JsonError::None; // 実際はもっと詳細にチェック
}
return JsonError::UnexpectedChar;
}
// コンパイル時にJSONを検証
static_assert(validate_json(R"({"key": "value"})") == JsonError::None);
static_assert(validate_json(R"([1, 2, 3])") == JsonError::None);
// static_assert(validate_json("invalid") == JsonError::None); // エラー!
コンパイル時計算のデバッグ
// static_assertでデバッグ
constexpr int compute_something(int x) {
int result = 0;
for (int i = 0; i < x; ++i) {
result += i * i;
}
return result;
}
// 期待値をチェック
static_assert(compute_something(0) == 0);
static_assert(compute_something(1) == 0);
static_assert(compute_something(3) == 5); // 0 + 1 + 4 = 5
static_assert(compute_something(5) == 30); // 0 + 1 + 4 + 9 + 16 = 30
まとめ
| キーワード | C++ | 用途 |
|---|---|---|
| constexpr | 11+ | コンパイル時/実行時両用 |
| if constexpr | 17 | コンパイル時分岐 |
| constexpr lambda | 17 | コンパイル時ラムダ |
| consteval | 20 | コンパイル時専用 |
| constinit | 20 | 静的初期化保証 |
C++20のconstexprは強力すぎて、もはや実行時コードをそのままコンパイル時に持っていけるレベル。
次回はコンセプトを使った型制約について解説する。
続きが気になったら、いいね・ストックしてもらえると嬉しいです!