はじめに
C++のテンプレート制約技術の進化を、SFINAE(C++98〜)からConcepts(C++20)まで追いかける。
SFINAEは「黒魔術」と呼ばれることもあるけど、Conceptsを使えばスッキリ書けるようになる。
パート1: SFINAE
SFINAE = Substitution Failure Is Not An Error(置換失敗はエラーではない)
1. enable_ifによる制約
#include <type_traits>
// 整数型のみ受け付ける
template<typename T, typename = enable_if_t<is_integral_v<T>>>
T double_value(T x) {
return x * 2;
}
double_value(5); // OK
// double_value(3.14); // コンパイルエラー
2. decltype SFINAEによる型検出
// size() メソッドを持つか検出
template<typename T>
auto has_size_impl(int) -> decltype(declval<T>().size(), true_type{});
template<typename T>
false_type has_size_impl(...);
template<typename T>
struct has_size : decltype(has_size_impl<T>(0)) {};
// has_size<vector<int>>::value == true
// has_size<int>::value == false
3. void_tを使った検出イディオム
template<typename, typename = void>
struct has_begin_end : false_type {};
template<typename T>
struct has_begin_end<T, void_t<
decltype(declval<T>().begin()),
decltype(declval<T>().end())
>> : true_type {};
4. enable_ifでオーバーロード制御
template<typename T>
enable_if_t<is_arithmetic_v<T>, string>
type_category(T) { return "arithmetic"; }
template<typename T>
enable_if_t<is_class_v<T> && !is_same_v<T, string>, string>
type_category(T) { return "class"; }
template<typename T>
enable_if_t<is_same_v<T, string>, string>
type_category(T) { return "string"; }
パート2: C++20 Concepts
5. 基本的なコンセプト定義
template<typename T>
concept Numeric = is_arithmetic_v<T>;
template<typename T>
concept Integral = is_integral_v<T>;
template<typename T>
concept FloatingPoint = is_floating_point_v<T>;
6. requires式を使ったコンセプト
template<typename T>
concept Addable = requires(T a, T b) {
{ a + b } -> convertible_to<T>;
};
template<typename T>
concept Printable = requires(T x, ostream& os) {
{ os << x } -> same_as<ostream&>;
};
7. Containerコンセプト
template<typename T>
concept Container = requires(T c) {
typename T::value_type;
typename T::iterator;
{ c.begin() } -> same_as<typename T::iterator>;
{ c.end() } -> same_as<typename T::iterator>;
{ c.size() } -> convertible_to<size_t>;
};
8. コンセプトを使った関数
// 制約付きテンプレート
template<Numeric T>
T add(T a, T b) {
return a + b;
}
// コンテナの合計
template<Container C>
auto container_sum(const C& c) {
typename C::value_type sum{};
for (const auto& elem : c) {
sum += elem;
}
return sum;
}
9. requires節
template<typename T>
requires Integral<T>
T factorial_concept(T n) {
if (n <= 1) return 1;
return n * factorial_concept(n - 1);
}
10. auto + requires (簡略記法)
// 最も簡潔な書き方
auto process_numeric(Numeric auto x) {
return x * 2;
}
process_numeric(10); // OK
process_numeric(3.5); // OK
// process_numeric("hello"); // エラー
11. コンセプトの組み合わせ
template<typename T>
concept OrderedContainer = Container<T> && requires(T c) {
{ *c.begin() < *c.begin() } -> convertible_to<bool>;
};
12. 制約付きクラス
template<Numeric T>
class NumericWrapper {
T value;
public:
constexpr NumericWrapper(T v) : value(v) {}
constexpr T get() const { return value; }
constexpr NumericWrapper operator+(const NumericWrapper& other) const {
return NumericWrapper(value + other.value);
}
};
NumericWrapper<int> w1(10); // OK
// NumericWrapper<string> w2; // エラー
SFINAE vs Concepts 比較
SFINAEの問題点
// 読みにくい...
template<typename T, typename = enable_if_t<
is_integral_v<T> && !is_same_v<T, bool>>>
T process(T x) { return x * 2; }
Conceptsなら
// 読みやすい!
template<typename T>
concept IntegralButNotBool = integral<T> && !same_as<T, bool>;
auto process(IntegralButNotBool auto x) {
return x * 2;
}
実行結果
=== SFINAE基本 ===
double_value(5): 10
=== 型検出 ===
has_size<vector<int>>: 1
has_size<int>: 0
has_begin_end<vector<int>>: 1
has_begin_end<int>: 0
=== オーバーロード制御 ===
type_category(42): arithmetic
type_category(3.14): arithmetic
type_category(string): string
type_category(vector<int>): class
=== 基本コンセプト ===
Numeric<int>: 1
Numeric<string>: 0
Integral<int>: 1
Integral<double>: 0
=== コンセプト関数 ===
add(3, 4): 7
add(1.5, 2.5): 4
container_sum(vector): 15
=== コンテナコンセプト ===
Container<vector<int>>: 1
Container<int>: 0
OrderedContainer<vector<int>>: 1
エラーメッセージの改善
SFINAE
error: no matching function for call to 'process'
note: candidate template ignored: requirement
'std::is_integral_v<std::basic_string<char>>' was not satisfied
Concepts
error: constraints not satisfied for class template 'NumericWrapper'
note: because 'std::string' does not satisfy 'Numeric'
note: because 'is_arithmetic_v<std::string>' evaluated to false
まとめ
| 特徴 | SFINAE | Concepts |
|---|---|---|
| 可読性 | 低い | 高い |
| エラーメッセージ | 難解 | 明確 |
| 再利用性 | 難しい | 簡単 |
| C++バージョン | C++98〜 | C++20〜 |
C++20が使えるなら、Conceptsを積極的に使いましょう!