C++ TMP (テンプレートメタプログラミング) シリーズ
| Part1 constexpr | Part2 Concepts | Part3 Variadic | Part4 型リスト | Part5 正規表現 |
|---|---|---|---|---|
| ✅ | 👈 Now |
はじめに
C++のテンプレートは強力だけど、エラーメッセージが地獄だった。
error: no matching function for call to 'sort'
note: candidate template ignored: substitution failure [with T = MyClass]:
no member named 'operator<' in 'MyClass'
...(数十行のエラーが続く)
C++20のコンセプトでこれが劇的に改善された。
コンセプトとは
型の要件を宣言的に記述する機能。
// コンセプトの定義
template<typename T>
concept Addable = requires(T a, T b) {
{ a + b } -> std::same_as<T>;
};
// コンセプトを使った関数
template<Addable T>
T add(T a, T b) {
return a + b;
}
add(1, 2); // OK
add(1.5, 2.5); // OK
struct NoAdd {};
add(NoAdd{}, NoAdd{}); // 明確なエラー: "NoAdd does not satisfy Addable"
4つの構文
// 1. requires句
template<typename T>
requires Addable<T>
T add1(T a, T b) { return a + b; }
// 2. テンプレートパラメータ
template<Addable T>
T add2(T a, T b) { return a + b; }
// 3. 省略形(関数パラメータ)
auto add3(Addable auto a, Addable auto b) {
return a + b;
}
// 4. 後置requires句
template<typename T>
T add4(T a, T b) requires Addable<T> {
return a + b;
}
どれを使ってもOK。好みで選ぶ。
標準コンセプト
<concepts>ヘッダに多数定義されている。
#include <concepts>
// 基本コンセプト
template<std::integral T> // 整数型
void process_int(T value);
template<std::floating_point T> // 浮動小数点型
void process_float(T value);
template<std::signed_integral T> // 符号付き整数
void process_signed(T value);
template<std::unsigned_integral T> // 符号なし整数
void process_unsigned(T value);
主な標準コンセプト
| コンセプト | 意味 |
|---|---|
same_as<T, U> |
TとUが同じ型 |
derived_from<D, B> |
DがBの派生型 |
convertible_to<From, To> |
FromからToに変換可能 |
integral |
整数型 |
floating_point |
浮動小数点型 |
copyable |
コピー可能 |
movable |
ムーブ可能 |
regular |
正則型(デフォルト構築、コピー、等価比較可能) |
requires式
コンセプトの本体で使う要件の記述方法。
単純要件
template<typename T>
concept HasSize = requires(T t) {
t.size(); // この式が有効であること
};
型要件
template<typename T>
concept HasValueType = requires {
typename T::value_type; // この型が存在すること
};
複合要件
template<typename T>
concept Indexable = requires(T t, size_t i) {
// 式が有効で、結果の型がT::value_typeへ変換可能
{ t[i] } -> std::convertible_to<typename T::value_type>;
};
ネストされた要件
template<typename T>
concept Container = requires(T t) {
typename T::value_type;
typename T::iterator;
{ t.begin() } -> std::same_as<typename T::iterator>;
{ t.end() } -> std::same_as<typename T::iterator>;
{ t.size() } -> std::convertible_to<size_t>;
requires std::copyable<T>; // 追加の要件
};
コンセプトの合成
論理演算子
// AND: &&
template<typename T>
concept SignedNumber = std::integral<T> && std::signed_integral<T>;
// OR: ||
template<typename T>
concept Number = std::integral<T> || std::floating_point<T>;
// NOT: !
template<typename T>
concept NotPointer = !std::is_pointer_v<T>;
実践例
template<typename T>
concept Arithmetic = requires(T a, T b) {
{ a + b } -> std::same_as<T>;
{ a - b } -> std::same_as<T>;
{ a * b } -> std::same_as<T>;
{ a / b } -> std::same_as<T>;
} && std::regular<T>;
template<Arithmetic T>
class Vector3 {
T x, y, z;
public:
constexpr Vector3(T x, T y, T z) : x(x), y(y), z(z) {}
constexpr Vector3 operator+(const Vector3& other) const {
return {x + other.x, y + other.y, z + other.z};
}
constexpr T dot(const Vector3& other) const {
return x * other.x + y * other.y + z * other.z;
}
};
オーバーロード解決
コンセプトでより制約の強いオーバーロードが優先される。
template<typename T>
void print(T value) {
std::cout << "generic: " << value << "\n";
}
template<std::integral T>
void print(T value) {
std::cout << "integral: " << value << "\n";
}
template<std::signed_integral T>
void print(T value) {
std::cout << "signed integral: " << value << "\n";
}
print(3.14); // generic: 3.14
print(42u); // integral: 42
print(-10); // signed integral: -10
サブセット関係が自動的に認識される。
generic < integral < signed_integral
< unsigned_integral
< floating_point
実践例1: イテレータコンセプト
template<typename I>
concept Iterator = requires(I i) {
typename std::iter_value_t<I>;
{ *i } -> std::same_as<std::iter_reference_t<I>>;
{ ++i } -> std::same_as<I&>;
{ i++ };
};
template<typename I>
concept ForwardIterator = Iterator<I> &&
std::copyable<I> &&
requires(I i) {
{ i == i } -> std::convertible_to<bool>;
};
template<typename I>
concept RandomAccessIterator = ForwardIterator<I> &&
requires(I i, I j, std::iter_difference_t<I> n) {
{ i + n } -> std::same_as<I>;
{ i - n } -> std::same_as<I>;
{ i - j } -> std::same_as<std::iter_difference_t<I>>;
{ i[n] } -> std::same_as<std::iter_reference_t<I>>;
{ i < j } -> std::convertible_to<bool>;
};
実践例2: Rangeコンセプト
template<typename R>
concept Range = requires(R r) {
{ std::ranges::begin(r) } -> Iterator;
{ std::ranges::end(r) };
};
template<typename R>
concept SizedRange = Range<R> && requires(R r) {
{ std::ranges::size(r) } -> std::convertible_to<size_t>;
};
template<SizedRange R>
void process_sized(R&& range) {
auto size = std::ranges::size(range);
std::cout << "Size: " << size << "\n";
}
process_sized(std::vector{1, 2, 3});
process_sized(std::array{1, 2, 3, 4, 5});
実践例3: シリアライザブル
template<typename T>
concept Serializable = requires(T t, std::ostream& os, std::istream& is) {
{ t.serialize(os) } -> std::same_as<void>;
{ T::deserialize(is) } -> std::same_as<T>;
};
template<typename T>
concept HasToString = requires(T t) {
{ t.to_string() } -> std::convertible_to<std::string>;
};
// 両方を満たす型用の特殊化
template<typename T>
requires Serializable<T> && HasToString<T>
void save_with_log(T& obj, std::ostream& os) {
std::cout << "Saving: " << obj.to_string() << "\n";
obj.serialize(os);
}
実践例4: 数学演算
template<typename T>
concept Numeric = requires(T a, T b) {
{ a + b } -> std::convertible_to<T>;
{ a - b } -> std::convertible_to<T>;
{ a * b } -> std::convertible_to<T>;
{ a / b } -> std::convertible_to<T>;
{ -a } -> std::convertible_to<T>;
{ a == b } -> std::convertible_to<bool>;
{ a < b } -> std::convertible_to<bool>;
};
template<typename T>
concept NumericWithSqrt = Numeric<T> && requires(T a) {
{ std::sqrt(a) } -> std::convertible_to<T>;
};
template<NumericWithSqrt T>
T distance(T x1, T y1, T x2, T y2) {
T dx = x2 - x1;
T dy = y2 - y1;
return std::sqrt(dx * dx + dy * dy);
}
SFINAEとの比較
Before: SFINAE (C++17以前)
// 読みにくい...
template<typename T>
typename std::enable_if<
std::is_integral_v<T> && std::is_signed_v<T>,
T
>::type
abs_sfinae(T value) {
return value < 0 ? -value : value;
}
// もっと読みにくい...
template<typename T,
typename = std::enable_if_t<std::is_integral_v<T>>>
T double_sfinae(T value) {
return value * 2;
}
After: コンセプト (C++20)
// すっきり!
template<std::signed_integral T>
T abs_concept(T value) {
return value < 0 ? -value : value;
}
template<std::integral T>
T double_concept(T value) {
return value * 2;
}
エラーメッセージの改善
Before: SFINAE
error: no matching function for call to 'process'
note: candidate template ignored: substitution failure
[with T = MyClass]: no type named 'type' in
'std::enable_if<false, void>'
After: コンセプト
error: constraints not satisfied for function template 'process'
note: because 'MyClass' does not satisfy 'Hashable'
note: because 'std::hash<MyClass>{}(val)' would be invalid
何が足りないのか一目瞭然。
高度なテクニック
条件付きコンセプト
template<typename T, typename U = T>
concept Addable = requires(T t, U u) {
{ t + u };
};
// 同じ型同士
static_assert(Addable<int>);
// 異なる型
static_assert(Addable<int, double>);
static_assert(!Addable<std::string, int>);
コンセプトのエイリアス
template<typename T>
concept InputRange = std::ranges::input_range<T>;
template<typename T>
concept OutputRange = std::ranges::output_range<T, std::ranges::range_value_t<T>>;
template<InputRange R>
auto first(R&& range) {
return *std::ranges::begin(range);
}
まとめ
| 機能 | メリット |
|---|---|
| 宣言的な型制約 | コードの意図が明確 |
| エラーメッセージ改善 | デバッグが楽 |
| オーバーロード解決 | 自然な特殊化 |
| 合成可能 | 再利用性 |
コンセプトはテンプレートの民主化。これまでTMPは上級者向けだったけど、コンセプトで敷居が下がった。
次回はバリアディックテンプレートの魔術を解説する。
続きが気になったら、いいね・ストックしてもらえると嬉しいです!