C++/コンセプト
```wikitext
コンセプトの概要
編集コンセプトとは何か
編集コンセプト (concept) は、C++20から導入された新しい言語機能です。コンセプトは、型が満たすべき一連の要件を定義するもので、これによりジェネリックプログラミングにおいてテンプレート引数として渡される型がその要件を満たしているかどうかをコンパイル時に検査できるようになります。
コンセプトの役割と目的
編集コンセプトの主な役割は、ジェネリックコードの安全性と表現力を高めることです。安全性の向上は、不適切な型がテンプレート引数として渡された場合にコンパイルエラーを発生させることで実現されます。表現力の向上は、コンセプトによってコードの意図がより明確に表現できるためです。
コンセプトの目的は次の3点に集約されます。
- コンパイル時の型検査を可能にする。
- ジェネリックコードの意図を明確に表現する。
- 適切なエラーメッセージを出力する。
コンセプトによるジェネリックプログラミングの利点
編集コンセプトを使うことで、以下のような利点が得られます。
- 安全性の向上
- コンパイル時に型の要件を検査できるため、実行時エラーを減少させることができます。
- 表現力の向上
- コンセプトによって要件を明示的に表現することで、コードの意図がより明確になります。
- エラーメッセージの改善
- コンセプトに基づいたエラーメッセージがコンパイラによって生成され、エラーの原因を特定しやすくなります。
- コード再利用性の向上
- 要件をコンセプトとして抽出することで、異なる場所で再利用が可能になります。
- 拡張性の向上
- 新しいコンセプトを定義して要件を追加することで、ジェネリックコードの柔軟性を高めることができます。
コンセプトの種類
編集C++20には、多くの標準コンセプトが用意されています。以下に主なコンセプトの分類を示します。
簡単なコンセプト (EqualityComparable、LessThanComparable など)
編集簡単なコンセプトは、型が等値比較や大小比較をサポートするかを示します。代表的なコンセプトは次のとおりです。
EqualityComparable
==
と!=
演算子をサポートする型。EqualityComparableWith
- 2つの型の間で
==
と!=
演算子がサポートされる。 LessThanComparable
<
演算子をサポートする型。LessThanComparableWith
- 2つの型の間で
<
演算子がサポートされる。
これらは、ソート済みコンテナやアルゴリズムの実装で用いられます。
コンテナ向けのコンセプト (Sequence、AssociativeContainer など)
編集コンテナ向けのコンセプトは、型がコンテナの要件を満たすかどうかを示します。代表例は次のとおりです。
Sequence
- 順序付きコンテナ (例:
vector
、deque
) の要件を満たす型。 RandomAccessRange
- ランダムアクセス可能な範囲をサポートする型。
AssociativeContainer
- 値に対する関数射影を提供するコンテナ (例:
map
、set
) の要件を満たす型。 UnorderedAssociativeContainer
- ハッシュを使用するコンテナの要件を満たす型。
これらは、関数テンプレートのパラメータとしてコンテナを使用する際に適用されます。
イテレータ向けのコンセプト (InputIterator、BidirectionalIterator など)
編集イテレータ向けのコンセプトは、特定のイテレータ型が満たすべき要件を定義します。代表的なものは次のとおりです。
InputIterator
- 単方向イテレータの要件を満たす型。
OutputIterator
- 出力イテレータの要件を満たす型。
ForwardIterator
- 前方向イテレータの要件を満たす型。
BidirectionalIterator
- 双方向イテレータの要件を満たす型。
RandomAccessIterator
- ランダムアクセスイテレータの要件を満たす型。
これらは、標準アルゴリズム (例: std::copy
、std::sort
) で広く使用されています。
その他のコンセプト (Callable、Predicate など)
編集その他の汎用コンセプトも存在します。
Callable
- 関数呼び出し可能な型。
Predicate
- 真偽値を返す関数オブジェクトとして使用される型。
Range
- 範囲をサポートする型。
Semiregular
- コピー構築可能かつデフォルト構築可能な型。
Regular
- 値の等値比較ができ、コピー代入可能な型。
これらのコンセプトはアルゴリズムやユーティリティ関数で使用されます。例えば、std::all_of
や std::find_if
では Predicate
コンセプトが使われます。
コンセプトの定義方法
編集概念的制約 (concept constraints) の記述
編集コンセプトは、型が満たすべき制約を表現します。制約は次のように記述されます。
template<typename T> concept concept_name = /* 制約条件 */;
たとえば、EqualityComparable
は次のように定義されます。
template<class T> concept EqualityComparable = requires(T a, T b) { { a == b } -> std::convertible_to<bool>; // a == b の結果がブール値に変換可能 { a != b } -> std::convertible_to<bool>; // a != b の結果もブール値に変換可能 };
この定義は型 T
が ==
と !=
演算子をサポートすることを表しています。
コンセプト定義の構文
編集コンセプトは以下の構文で定義されます。
concept concept_name = constraint_expression;
concept_name
: コンセプトの名前constraint_expression
: 型が満たすべき制約条件を記述する論理式
制約条件の例:
- 型トレイト
std::is_integral<T>
,std::is_default_constructible<T>
など- 簡単な要件
sizeof(T) <= 16
,std::has_virtual_destructor<T>
など- 合成要件
EqualityComparable<T>
,std::derived_from<T, Base>
など- 演算子要件
a == b
,a != b
,a < b
など- メンバ関数要件
a.empty()
,a.size()
,T::value_type
など
コンパウンドコンセプト (compound concepts)
編集複数のコンセプトを組み合わせて新しいコンセプトを作成することもできます。例として次に、複数のコンセプトを組み合わせる方法を示します。
template<typename T> concept Hashable = requires(T a) { { std::hash<T>{}(a) } -> std::convertible_to<std::size_t>; }; template<typename T> concept CustomConcept = EqualityComparable<T> && Hashable<T>;
この例では、CustomConcept
という新しいコンセプトが EqualityComparable
と Hashable
の両方を満たす型として定義されています。このように、コンパウンドコンセプトはジェネリックコードの柔軟性を高めるために使用されます。
コンセプトの使用方法
編集テンプレート宣言におけるコンセプトの使用
編集テンプレート関数やクラスの定義において、コンセプトを使用することで、型パラメータの制約を明示的に指定できます。これにより、意図しない型の使用を防ぐことができます。
以下に、コンセプトを使用したテンプレート関数の例を示します。
template<EqualityComparable T> bool are_equal(const T& a, const T& b) { return a == b; }
この例では、テンプレート引数 T
が EqualityComparable
コンセプトを満たす型であることを要求しています。この関数は、==
演算子が使用できる型のみ受け入れます。
requires
式の使用
編集
コンセプトは requires
式を用いて関数内で直接使用することもできます。これにより、関数内で特定の条件を満たしているかどうかを柔軟にチェックできます。
template<typename T> requires std::integral<T> T add(T a, T b) { return a + b; }
この例では、T
が std::integral
コンセプトを満たす場合にのみ、add
関数が使用できるようになっています。requires
キーワードを使用することで、テンプレート宣言に制約を簡潔に追加できます。
概念ベースのオーバーロード
編集コンセプトを使用すると、関数のオーバーロードを簡単に定義できます。たとえば、異なるコンセプトを持つ型に対して異なる処理を提供することが可能です。
template<typename T> requires std::integral<T> void print_info(const T& value) { std::cout << "Integral value: " << value << '\n'; } template<typename T> requires std::floating_point<T> void print_info(const T& value) { std::cout << "Floating-point value: " << value << '\n'; }
この例では、print_info
関数は std::integral
型と std::floating_point
型で異なる出力を行います。コンセプトに基づいて関数のオーバーロードを選択できるため、コードの意図がより明確になります。
- まとめ
C++20 のコンセプトは、型パラメータに制約を与えることでジェネリックプログラミングをより安全かつ表現力豊かにします。コンセプトを用いることで、意図しない型の使用を防ぎ、コードの可読性と保守性を向上させることができます。また、エラーメッセージも改善され、開発者は問題の原因を特定しやすくなります。
コンセプトは今後の C++ プログラミングにおいて、ますます重要な役割を果たしていくでしょう。これにより、ジェネリックコードをより安全で強力なものにするための道が開かれます。