C++/コンセプト
コンセプトの概要
編集コンセプトとは何か
編集コンセプト(concept)は、C++20から導入された新しい言語機能です。コンセプトは、型が満たすべき一連の要件を定義するものです。これにより、ジェネリックプログラミングにおいて、テンプレート引数として渡される型がその要件を満たしているかどうかをコンパイル時に検査できるようになります。
コンセプトの役割と目的
編集コンセプトの主な役割は、ジェネリックコードの安全性と expressivenessを高めることです。安全性が高まるのは、不適切な型がテンプレート引数として渡された場合にコンパイルエラーが発生するためです。expressivenessが向上するのは、コンセプトによってコードの意図がより明確に表現できるようになるためです。
コンセプトの目的は、以下の3つにまとめられます。
- コンパイル時の型検査を可能にする
- ジェネリックコードの意図を明確に表現する
- 適切なエラーメッセージを出力する
コンセプトによるジェネリックプログラミングの利点
編集コンセプトを使うことで、ジェネリックプログラミングにおいて以下の利点が得られます。
- 安全性の向上
- コンパイル時に型の要件を検査できるため、実行時エラーを減らすことができます。
- expressivenessの向上
- コンセプトによって要件を明示的に表現できるため、コードの意図が明確になります。
- エラーメッセージの改善
- コンセプトに基づいたエラーメッセージがコンパイラから出力されるため、ユーザーがエラーの原因を特定しやすくなります。
- コード再利用性の向上
- 要件をコンセプトとして抽出することで、複数の場所で再利用できるようになります。
- 拡張性の向上
- 新しいコンセプトを定義して要件を追加することで、ジェネリックコードの用途を広げることができます。
コンセプトの種類
編集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
- 述語(bool値を返す関数オブジェクト)の要件を満たす型
Range
- 範囲の要件を満たす型
Semiregular
- コピー構築可能でデフォルト構築可能な型
Regular
- 値の等値比較ができてコピー代入可能な型
これらのコンセプトは、アルゴリズムやユーティリティ関数で広く使われています。たとえば、std::all_of
やstd::find_if
ではPredicate
コンセプトが使用されています。
コンセプトの定義方法
編集概念的制約(concept constraints)の記述
編集コンセプトは、型が満たすべき一連の制約条件を表します。これらの制約条件は、概念的制約(concept constraints)と呼ばれます。概念的制約は以下のように記述されます。
template<typename T> concept concept_name = /* 制約条件 */;
制約条件には、型が満たすべき要件を論理式で記述します。論理式では、型トレイト、型の有する演算子、型メンバ関数の有無などを使って要件を表現します。
例えば、EqualityComparable
コンセプトは次のように定義されています。
template<class T> concept EqualityComparable = requires(T a, T b) { { a == b } -> Boolean; // a == bの結果はブール値に変換可能 { a != b } -> Boolean; // a != bの結果もブール値に変換可能 };
この定義では、型T
が等値比較演算子==
と!=
をサポートしている必要があることを表しています。
コンセプト定義の構文
編集コンセプトは、以下の構文で定義されます。
concept concept_name = constraint_expression;
concept_name
はコンセプトの名前です。constraint_expression
は、型が満たすべき制約条件を表す論理式です。
制約条件には、以下のような要素が含まれます。
- 型トレイト
is_integral<T>
,is_default_constructible<T>
など- 簡単な要件
sizeof(T) <= 16
,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)
編集コンセプトは他のコンセプトから合成することもできます。このようなコンセプトをコンパウンドコンセプト(compound concepts)と呼びます。
コンパウンドコンセプトは、以下のように&&
、||
、!
を使って定義されます。
template<class T> concept C = /* 制約条件1 */ && /* 制約条件2 */; template<class T, class U> concept D = /* 制約条件1 */ || /* 制約条件2 */; template<class T> concept E = /* 制約条件 */ && !C<T>;
コンパウンドコンセプトを使うことで、必要な制約条件を簡潔に表現できます。
コンセプトの使用例
編集関数テンプレートでのコンセプトの利用
編集コンセプトは主に関数テンプレートでの型制約に使われます。関数テンプレートの型パラメータにコンセプトを指定することで、その関数が要求する制約条件を明示的に表現できます。
template<EqualityComparable T> void f(T a, T b) { if (a == b) { /* ... */ } }
この例では、EqualityComparable
コンセプトを使って、関数f
が等値比較をサポートする型のみを受け入れることを示しています。型T
がEqualityComparable
の制約条件を満たさない場合、コンパイルエラーになります。
コンセプトは関数パラメータの制約にも使えます。
template<InputIterator Iter, Predicate<typename std::iterator_traits<Iter>::value_type> Pred> Iter find_if(Iter first, Iter last, Pred pred) { /* ... */ }
この例では、InputIterator
とPredicate
の2つのコンセプトを使っています。Iter
は入力イテレータの要件を、Pred
はIter
の要素型に対する述語の要件を満たす必要があります。
クラステンプレートでのコンセプトの利用
編集コンセプトはクラステンプレートでも使用できます。これにより、クラステンプレートがテンプレート引数として受け入れる型を制限できます。
template<RandomAccessIterator Iter, Semiregular T> class vector { /* ... */ };
この例では、vector
クラステンプレートは、RandomAccessIterator
要件を満たすイテレータ型とSemiregular
要件を満たす値型のみを受け入れます。
クラステンプレートのメンバ関数にもコンセプトを使うことができます。
template<AssociativeContainer C> class X { public: template<typename K, typename V> requires Semiregular<std::pair<const K, V>> void insert(K&& key, V&& value) { /* ... */ } };
この例では、insert
メンバ関数は、std::pair
型がSemiregular
要件を満たす場合にのみ呼び出せます。
演算子オーバーロードでのコンセプトの利用
編集コンセプトは演算子のオーバーロード関数でも使えます。これにより、特定の型に対してのみ演算子をオーバーロードできます。
template<RegularInvocable BinaryOp, typename T> constexpr T foldl(BinaryOp op, T init, const std::vector<T>& v) { for (const auto& x : v) { init = op(init, x); // op(T, T) -> Tが成り立つ必要がある } return init; }
この例では、foldl
関数の引数op
はRegularInvocable
コンセプトを満たす必要があります。つまり、op
は2つのT
型の引数を取り、T
型の値を返す二項演算である必要があります。このようにコンセプトを使うことで、安全性が高まります。
コンセプトによるエラーメッセージの改善
編集コンセプトを使うとエラーメッセージが改善され、ユーザーがエラーの原因を特定しやすくなります。次の例を見てみましょう。
template<typename T> concept C = /* ... */; template<typename U> requires C<U> void f(U u) { /* ... */ } struct X {}; int main() { X x; f(x); // エラー! Xは Cの要件を満たしていない }
この場合、コンパイラは次のようなエラーメッセージを出力するでしょう。
</syntaxhighlight> error: constraints not satisfied for 'f(X) [with U=X]' note: because 'X' does not satisfy 'C' </syntaxhighlight>
このエラーメッセージから、関数f
の制約条件C
をX
が満たしていないことが分かります。つまり、この関数呼び出しがコンパイルエラーになった理由が明確に示されています。
対照的に、コンセプトを使わない場合のエラーメッセージは以下のようになります。
template<typename T, typename = std::enable_if_t</* 制約条件 */>> void f(T t) { /* ... */ } error: no matching function for call to 'f(X)'
この場合、エラーの原因があまり明確ではありません。コンパイラは単に関数f
の呼び出しがマッチしないと報告するだけです。制約条件を満たしていない理由は分かりません。
コンセプトを使うことで、エラーメッセージが大幅に改善され、ユーザーがエラーの原因を特定しやすくなります。これは、ジェネリックコードのデバッグを容易にするという重要な利点です。
コンセプトとtypedefs/usingの比較
編集古典的なテンプレートプログラミングとの違い
編集C++20以前のテンプレートプログラミングでは、テンプレート引数の型制約を表すためにtypedefs
やusing
エイリアスが使われていました。
// typedefs/usingによる制約の表現 template<typename T> using IsIntegral = std::is_integral<T>::value; template<typename T> using IsEqualityComparable = std::is_same<typename std::decay<T>::type, T>::value && std::is_default_constructible<T>::value && /* ... */; // 制約付きのテンプレート template<typename T, typename = std::enable_if_t<IsIntegral<T>>> T add(T a, T b) { return a + b; }
このアプローチには以下の問題がありました。
- 制約条件を表す型特性を明示的に定義する必要がある
- エラーメッセージが分かりにくい
- テンプレートの意図が明確でない
コンセプトの導入により、これらの問題が解決されました。コンセプトでは制約条件を直接的に表現でき、意図が明確になり、分かりやすいエラーメッセージが出力されるようになりました。
コンセプトの長所と短所
編集- 長所
- 制約条件を直接的に表現できる
- コードの意図が明確になる
- 分かりやすいエラーメッセージが得られる
- コンセプトの再利用が可能
- コンパウンドコンセプトで複雑な制約を表現できる
- 短所
- C++20以降の機能なので、古いコンパイラでは使用できない
- 制約条件の記述が複雑になる可能性がある
- コンセプトの導入によるコードサイズの増加
全体として、コンセプトの導入によりジェネリックプログラミングが大きく改善されました。コンセプトを上手く活用することで、安全で expressivenessの高いコードを書くことができます。一方で、制約条件の記述が複雑になる可能性や、一時的な最適化の問題が生じる場合があることに注意が必要です。
コンセプトとモジュール
編集モジュールでのコンセプトのexport/import
編集C++20のモジュールシステムでは、コンセプトをモジュール間で export/import できます。これにより、コンセプトの再利用性が高まります。
モジュールインターフェース(.cppm
)ファイルでコンセプトをエクスポートするには、以下のようにします。
// concepts.cppm export module concepts; export concept EqualityComparable<typename T> { ... } export concept Range<typename T> { ... }
別のモジュールからこれらのコンセプトをインポートするには、以下のようにします。
// algorithms.cppm import concepts; export template<EqualityComparable T> bool is_permutation(const Range<T>& a, const Range<T>& b) { ... }
モジュールでコンセプトをエクスポート/インポートすることで、プロジェクト全体で一貫したコンセプトの定義と使用が可能になります。
モジュールとコンセプトの相互作用
編集モジュールは、コンセプトと密接に関係しています。モジュールのインターフェースでコンセプトを宣言すると、そのモジュールの実装ファイル内でのみ、そのコンセプトの定義を記述できます。
// concepts.cppm (モジュールインターフェース) export module concepts; export concept EqualityComparable<typename T>; // concepts.cpp (モジュール実装) module concepts; template<typename T> concept EqualityComparable = /* 定義 */;
このように、モジュールとコンセプトを組み合わせることで、コンセプトの可視性とカプセル化が実現できます。
モジュールは、コンパートメント化された独立したコンテキストを提供します。つまり、同じ名前のコンセプトが異なるモジュールで定義されていても、それぞれのモジュールで独立して扱われます。これにより、名前の衝突を避けることができます。
まとめ
編集コンセプトは、C++20で導入された強力な機能です。コンセプトを使うことで、ジェネリックプログラミングの安全性と expressivenessが大幅に向上しました。コンセプトの主な利点は以下の通りです。
- コンパイル時の型検査が可能になり、実行時エラーを減らせる
- コードの意図が明確になる
- エラーメッセージが分かりやすくなる
- コード再利用性が向上する
- 新しい要件を追加しやすくなり、拡張性が高まる
一方で、コンセプトの記述が複雑になる可能性や、一時的な最適化の問題が生じる場合があるため、注意が必要です。
今後、コンパイラの最適化が進めば、これらの短所は軽減されていくことが期待されます。モジュールシステムとの連携により、コンセプトの再利用性と一貫性も高まるでしょう。コンセプトは、C++がよりモダンでクリーンなジェネリックプログラミングを実現する上で、重要な役割を果たすことになるでしょう。