コンセプト (C++)

コンセプト（英: concept）は、プログラミング言語の機能。かつてC++への採用が検討されていたが、見送られた。ISO/IEC JTC1/SC22/WG21 C++ 標準化委員会によるC++0xの策定において2009年前半まで検討され、規格書のドラフトにも盛り込まれていたが、2009年7月13日の投票で削除されることが決まった。 C++では、テンプレートクラス・関数は引数とする型に必然的に何らかの制限を課す。例えば、STLコンテナは格納する型にデフォルトコンストラクタを要求する。Foo&型のオブジェクトを受ける関数にはFooのあらゆるサブタイプを渡せる、というようなクラス階層によって提供される動的な多態とは違い、テンプレート引数には、テンプレートが使う演算をサポートするクラスならば何でも渡すことが出来る。しかし、通常の関数の場合は引数に対する要求は明確である（先の例なら、Fooのサブタイプであること）が、テンプレートの場合は、オブジェクトが持たなければならないインターフェースはテンプレート定義の中に隠れてしまっている。コンセプトはテンプレート引数が持たなくてはならないインターフェースを明文化する機構を提供する。 コンセプトを導入する理由の一つは、エラーメッセージの質の改善である。テンプレートが必要とするインターフェースを持っていない型をプログラマが使おうとした場合、コンパイラはエラーを出す。しかし、その種のエラーは理解しがたいものになりやすく、特に初心者には非常に難解である。この理由としては、エラーメッセージにテンプレート引数が省略されずに表示され、非常に長いエラーメッセージが出力されてしまうことが多いことが挙げられる。コンパイラによっては、単純なミスが数キロバイトものエラーメッセージを出す結果になることもある。他の理由として、エラーメッセージがエラーの実際の理由を明確に示していないことがある、ということもある。例えば、コピーコンストラクタを持たないオブジェクトのvectorを構築しようとした場合、大抵の場合のエラーメッセージは「内包するオブジェクトにコピーコンストラクタを呼んでしまったvectorクラス」に言及するものになってしまう。熟練したプログラマでないと、本当のエラーが「vectorに渡した型がvectorの要件を満たしきれていないこと」がわからないだろう。 コンセプトが記載された最後の規格書ドラフトである N2914 の記述に基づいて説明する。 コンセプトは、名前付きの構造であり、型が提供しなければならない機能を指定する。この点は、オブジェクト指向プログラミングで、型の行えることの制限の定義を基底クラスにより行うのに似ている。しかしオブジェクト指向プログラミングとは違い、コンセプトの定義自体にはテンプレートに渡される型が明示的に関連付けられず、テンプレート定義の側で結びつけられる: template<LessThanComparable T> const T& min(const T &x, const T &y) { return x < y ? x : y; } テンプレート型引数にclassやtypenameを使い任意の型と指定するのではなく、前方で定義されたコンセプトであるLessThanComparableを使用している。テンプレート関数minに渡された型がコンセプトLessThanComparableを満たさない場合、コンパイルエラーとなり、テンプレート実体化に使われた型がLessThanComparableコンセプトに適合しなかったことが報告される。 より一般的なコンセプトの使用の記法は、以下のようになる: template<typename T> requires LessThanComparable<T> const T& min(const T &x, const T &y) { return x < y ? x : y; } requiresキーワードの後には、コンセプト宣言のリストが続く。これにより、複数の型を使うコンセプトを使用できる。また、requires !LessThanComparable<T>のように用いることも出来、あるコンセプトに合致する型の使用を禁止することが出来る。これらの機能は、テンプレートの特殊化と同様の方法で使用できる。一般的なテンプレートを少ない機能しか使わないものとして定義し、さらに多機能なコンセプトを用いた特殊化を用意することで、その機能を用いて高いパフォーマンスや高機能性を実現できる。 コンセプトは以下のように定義される: auto concept LessThanComparable<typename T> { bool operator<(T, T); } この例に含まれているキーワードautoは、コンセプトに記述された操作を提供するあらゆる型がコンセプトを満たすこととする、ということを示すものである。autoキーワードが無い場合、コンセプトを満たす型を宣言するのにコンセプトマップを使う必要がある。 このコンセプトは、「自身と同じ型の引数を二つ取り、boolを返す演算子 < を持つあらゆる型はLessThanComparableである」ということを示している。演算子は大域関数ではなく、型Tのメンバ関数でなくてはならない。 コンセプトは、複数の型を含むことも出来る。例えば、二つの型をとり、片方がもう片方の型へと変換できる、ということを表すコンセプトは: auto concept Convertible<typename T, typename U> { operator U(const T&); } これをテンプレートで使うためには、コンセプトの一般的使用の記法を使う必要がある: template<typename U, typename T> requires Convertible<T, U> U convert(const T& t) { return t; } コンセプトは複合できる。例えば、Regularというコンセプトが与えられている場合: concept InputIterator<typename Iter, typename Value> { require Regular<Iter>; Value operator*(const Iter&); Iter& operator++(Iter&); Iter operator++(Iter&, int); } InputIteratorコンセプトに渡されたテンプレート引数の一つ目は、Regularコンセプトを満たすことが確認される。 継承を行うように、コンセプトも他のコンセプトから派生できる。そして、クラスの継承のように、派生コンセプトも基底コンセプトの要件を満たす。派生コンセプトは、クラスの派生のように定義される: concept ForwardIterator<typename Iter, typename Value> : InputIterator<Iter, Value> { //ここに他の要件を追記する。 } コンセプトに型名 (typename)を結び付けることもできる。これにより、型名が使用可能であることをコンセプトの要件として表現できる: concept InputIterator<typename Iter> { typename value_type; typename reference; typename pointer; typename difference_type; require Regular<Iter>; require Convertible<reference, value_type>; reference operator*(const Iter&); // デリファレンス Iter& operator++(Iter&); // 前置インクリメント Iter operator++(Iter&, int); // 後置インクリメント // ... } コンセプトマップは、コンセプトに型を明示的に結びつけるのに使われる。これにより、型が（可能ならば）型定義を変えることなくコンセプトに適合することを示せる。例を挙げる: concept_map InputIterator<char*> { typedef char value_type; typedef char& reference; typedef char* pointer; typedef std::ptrdiff_t difference_type; }; このコンセプトマップは、InputIteratorコンセプトを満たす型としてchar*型を与えている。 柔軟性を高めるため、コンセプトマップ自体をテンプレートに出来る。以下の例は、あらゆるポインタ型を扱えるように拡張したものである: template<typename T> concept_map InputIterator<T*> { typedef T value_type; typedef T& reference; typedef T* pointer; typedef std::ptrdiff_t difference_type; }; さらに、コンセプトマップは、クラスに共通に関連付けられる関数定義などの構造を示す、ミニタイプとしても振る舞うことも出来る: concept Stack<typename X> { typename value_type; void push(X&, const value_type&); void pop(X&); value_type top(const X&); bool empty(const X&); }; template<typename T> concept_map Stack<std::vector<T> > { typedef T value_type; void push(std::vector<T>& v, const T& x) { v.push_back(x); } void pop(std::vector<T>& v) { v. pop_back(); } T top(const std::vector<T>& v) { return v. back(); } bool empty(const std::vector<T>& v) { return v. empty(); } }; このコンセプトマップは、Stackコンセプトを実装する型を引数とするテンプレートが、std::vectorを引数に取れるように定義している。std::vectorを使えるようにするため、各関数呼び出しをstd::vectorの関数の呼び出しに置き換えている。これを用いれば、究極的には、既に存在するオブジェクトの定義を変えることなく、それをテンプレート関数が使用するインターフェースに適合できる。 静的な表明を用いてコンセプトの各要件を確認できることもできる。実際には静的表明の機能は別の問題に焦点を当てているのであるが、この機能でテンプレートの要する要件を検証することができる。 axiomキーワードを用いて、コンセプトを実装する型が満たすべき性質を記述できる。これはコードの動作には寄与せず、コードチェッカやIDEへのヒントとしての利用が想定されている。 late_checkキーワードを用いて、部分的にコンセプトによるチェックを行わないように出来る。 いくつかのコンセプトは、ライブラリの使用によらずstd名前空間内に暗黙に定義される。 コンセプトをベースとした機能、およびコンセプトのライブラリも導入される予定であった。コンセプトの削除後はコンセプトを使わないように修正されている。修正後についてはC++0xを参照。 Boost C++ ライブラリでは、いくつか "範囲" のコンセプトを使用しているものがある。範囲は、リストの二点をもってリストを表現するものであり、コンテナにも似ている。順序付きのコンテナは範囲コンセプトから見れば上位にあり、順序付きコンテナから二つイテレータを持ってくれば範囲を定義できる。これらの考え方や、またこれを元に動作するアルゴリズムが、C++0x の標準ライブラリに組み込まれる予定である。しかし C++0x では、ライブラリだけではなく、範囲コンセプトを使う言語機能ももたらされることになる予定である。 for 文では、範囲の考え方を用いて簡単な反復が行えるようになる: int my_array[5] = { 1, 2, 3, 4, 5 }; for (int &x : my_array) { x *= 2; } 新形式の for ループでは、一つ目の部分で、範囲を反復するために使う変数を定義する。この変数は、一般の for ループで宣言される変数同様、ループの範囲にのみスコープを持つ。":" に続く二つ目の部分で、反復対象の範囲を指定する。この場合、C スタイルの配列を範囲コンセプトに変換するコンセプトマップがあるので、C 配列を範囲コンセプトとして渡すことが出来る。この機能は、 std::vector などの範囲コンセプトを満たすものなら何にでも適用できる。 C++0xにはコンセプトのライブラリが追加される予定であった。 <concepts> <type_traits>のコンセプト版。HasPlus(+演算子が使える)、DefaultConstructible(デフォルトコンストラクタによる構築が出来る)などの基本的な性質を表すコンセプトが集められている。 <container_concepts> STLコンテナを規定するコンセプト。 <iterator_concepts> イテレータを規定するコンセプト。 <memory_concepts> アロケータを規定するコンセプト。