コンセプトは、 C++プログラミング言語が提供するテンプレート機能の拡張機能です。[ 1 ]コンセプトは、コンパイル時に評価されるテンプレート パラメータに対する名前付きブール述語です。コンセプトは、テンプレート (クラステンプレート、関数テンプレート、クラス テンプレートのメンバ関数、変数テンプレート、またはエイリアス テンプレート) に関連付けられる場合があり、その場合は制約として機能します。つまり、テンプレート パラメータとして受け入れられる引数のセットを制限します。
概念の主な用途は以下のとおりです。
従来、テンプレートタイプの制約は、置換失敗はエラーではない(SFINAE)という表現を用いて示されていましたが、概念を用いることで、制約をより明確に表現することが可能になります。
関数テンプレートのシグネチャには、制約を使用できる 5 つの場所があります (以下に 1 ~ 5 のラベルが付けられています): [ 2 ]
テンプレート< Concept1 T >はConcept2 < T >を必要とします。Concept3を自動で呼び出します。myFunction ( Concept4を自動で呼び出します。param )はConcept5 < T >を必要とします。Concept1: 型制約。この種類は、テンプレート型パラメータclassを宣言するための型名または型定義に代わるものです。前二者の代わりに概念を使用する場合、型は制約されます。Concept2: 要件句。型制約が機能しない場合(例えば、概念が複数のパラメータを取る場合など)、要件句を使用してより詳細な制約を適用できます。Concept3、Concept4: 制約付きプレースホルダー型。同じ構文は、プレースホルダー変数(別名:auto変数)にも使用できます。C++20 では、パラメーター宣言でプレースホルダー型としてを使用する省略された関数テンプレートが追加されました。 [ 3 ]制約付きプレースホルダー型を使用すると、関数または変数の自動的に推論される戻り値の型に制約を設定できます。autoConcept5: 末尾の requires 句。この形式は と似ていますが、Concept2注目すべき例外が 1 つあります。末尾の requires 句は、クラス テンプレート内の関数に適用できます。これにより、関数は通常のテンプレートフリー関数として維持され、関数の末尾の requires 句に応じて有効または無効にすることができます。制約形式はConcept1、Concept2あらゆる種類のテンプレートで使用できます。
以下では、型に対する継承制約の上限として概念を使用する方法を示しますstd::derived_from<Derived, Base>。
import std ;using std :: derived_from ; using std :: vector ;class Player { // ... };// T は、継承の上限が Player である型である必要があります。// Player を継承しない型はすべてブロックされます。template < derived_from < Player > T > void processListOfPlayers ( const vector < T >& players ) { // ... }これはJavaの制約付きジェネリクスに似ており、次の例と同等です。
import java.util.List ;class Player { // ... }public class Example { // T は Player を継承する型に制限されますpublic static < T extends Player > void processListOfPlayers ( List < T > players ) { // ... } }以下は、C++ 標準ライブラリのヘッダーstd::equality_comparableにある、この概念の定義例です。この概念は、型 の左辺値と に対して、式とおよびその逆 と がコンパイルされ、その結果が「ブールテスト可能」の概念を満たす型に変換できるような任意の型によって満たされます。<concepts>TabTa == ba != bb == ab != a
/** * @namespace std * @brief C++ 標準ライブラリの名前空間*/ namespace std { /** * @internal * @concept _WeaklyEqualityComparableWith * @brief 次の概念は、equality_comparable を構築するために使用される実装の詳細です。 * @tparam T 比較する最初のテンプレート パラメーター * @tparam U 比較する 2 番目のテンプレート パラメーター */ template < typename T , typename U > concept _WeaklyEqualityComparableWith = requires ( const remove_reference < T >&a a 、const remove_reference < U >& b ) { { a == b } -> same_as < bool > ; { a != b } -> same_as < bool > ; { b == a } -> same_as < bool > ; { b != a } -> same_as < bool > ; };/** * @concept equality_comparable * @brief a == b、a != b、b == a、b != a をチェックし、コンパイルしてブール値に変換します。 * @tparam T 比較する型 */ template < typename T > concept equality_comparable = _WeaklyEqualityComparableWith < T , T > ; }この概念に基づいて制約された関数テンプレートは、次のように宣言できます。
// 制約付き省略関数テンプレート宣言// 制約付きプレースホルダー型 (上記の Concept4) を使用void f ( const equality_comparable auto & x );または
// 制約付き関数テンプレート宣言// 型制約を使用 (上記のコンセプト1) template < equality_comparable T > void f ( const T & x );そして、通常通り呼ばれることもあります。
// OK、int は equality_comparable f ( 42 ) を満たします。C++には、Javaのような型安全な可変引数関数がなく、型安全性に欠けるCスタイルの<cstdarg>しかありません。しかし、C++には可変引数テンプレートがあり、テンプレートパラメータの数に制限はありません。voidf(String...args)
TJava でパラメータを型( に類似)のみに制限するにはT... args、概念std::same_as[ 4 ]またはstd::convertible_to(変換可能であることを意図した型の場合) を使用できます。[ 5 ]
std :: same_asを使用します。// Java宣言と同等// <T> void fn(T... args) template < typename T > void fn ( same_as < T > auto ... args ) { // ... }これはさらに特定のタイプに限定することもできます。
using std :: convertible_to ; using std :: same_as ; using std :: string ;// Java宣言と同等// void foo(int... args) void foo ( same_as < int > auto ... args ) { // ... }// Java宣言と同等// void bar(String... args) void bar ( convertible_to < string > auto ... args ) { // ... }オブジェクト指向プログラミングにおいて、インターフェースは、クラスがそれを満たすために実装しなければならないメソッドシグネチャのセットを定義します。概念はインターフェースに似ており、型が様々な要件を満たすことを定義できます。
import std ;using std :: same_as ; using std :: floating_point ;template < typename T > concept Drawable = requires ( T t ) { { t . draw () } -> same_as < void > ; // T::draw は void を返す必要があります{ t . area () } -> floating_point ; // 任意の浮動小数点型に一致します};// クラス Circle は Drawable の概念を満たします// ただし、Circle は Drawable を満たすことを宣言する必要はありませんclass Circle { private : const double radius ; public : explicit Circle ( double r ) : radius { r } {}void draw () const { // 円を描画する... }double area () const noexcept { return std :: numbers :: pi * radius * radius ; } };// Drawable を満たす型のみ使用できます。template < Drawable T > void render ( const T & shape ) { // ... shape . draw (); std :: println ( "面積 {} の図形を描画しました" , shape . area ()); }これはJavaではおおよそ以下のようになります。
interface Drawable { void draw (); // 戻り値は void でなければならないdouble area (); // 戻り値は double でなければならない}// Circle は Drawable を実装することを明示的に宣言する必要がありますclass Circle implements Drawable { private final double radius ;public Circle ( double r ) { this.radius = r ; }@Override public void draw () { // 円を描画する... }@Override public double area () { return Math . PI * radius * radius ; } }// Drawable インターフェースを実装する型のみ使用できますstatic void render ( Drawable shape ) { // ... shape . draw (); System . out . printf ( "面積 %f の図形を描画しました" , shape . area ()); }C++ の概念はGo のインターフェースと非常によく似ています。Javaのインターフェースとは異なり、Go のインターフェースでは、型がインターフェースを実装することを宣言する必要はありません。型は、その要件をすべて実装している限り、暗黙的にインターフェースを満たします。[ 6 ]例えば、DrawableC++ のこの概念は、Go のインターフェースとほぼ同等です。
import "math"type Drawable interface { Draw () Area () float64 }type Circle struct { radius float64 }func ( c Circle ) Draw () { // 円を描画する... }func ( c Circle ) Area ( ) float64 { return math.Pi * c.radius * c.radius }プログラマがテンプレートの要件を満たさないテンプレート引数を使用しようとすると、コンパイラはエラーを生成します。概念が使用されていない場合、このようなエラーは理解しにくいことがよくあります。なぜなら、エラーは呼び出しのコンテキストではなく、型が使用された内部の、多くの場合深くネストされた実装コンテキストで報告されるからです。
例えば、は最初の 2 つの引数がランダムアクセス イテレータであることを要求します。引数がイテレータでない場合、または異なるカテゴリのイテレータである場合、そのパラメータを双方向イテレータとして使用しようとするとエラーが発生します。二重リンクリストを実装し、ランダムアクセスではない を考えてみましょう。std::sortstd::sortstd::list<T>
std :: listを使用します。// std::list は通常、イテレータがランダムアクセスではない二重リンクリストです。list < int > l = { 2 , 1 , 3 }; std :: sort ( l . begin (), l . end ());概念を除いた典型的なコンパイラ診断メッセージは50行以上に及び、2つのイテレータを減算しようとする式のコンパイル失敗から始まります。
'void std::__sort(_RandomAccessIterator, _RandomAccessIterator, _Compare) [with _RandomAccessIterator = std::_List_iterator<int>; _Compare = __gnu_cxx::__ops::_Iter_less_iter]' のインスタンス化において: エラー: 'operator-' に一致するものが見つかりません (オペランドの型は 'std::_List_iterator<int>' と 'std::_List_iterator<int>' です) std::__lg(__last - __first) * 2, [..]
概念が使用されている場合、エラーは呼び出しのコンテキストで検出され、報告されます。
エラー: 関数 'void std::sort(_RAIter, _RAIter) [with _RAIter = std::_List_iterator<int>]' を呼び出すことができません 注: コンセプト 'RandomAccessIterator()' が満たされませんでした
概念を用いることで、 SFINAEやタグディスパッチの代替手段として、テンプレート引数の特性に基づいて関数テンプレートのオーバーロードやクラステンプレートの特殊化を選択できる。引数が複数の概念を満たす場合、より制約の厳しい概念に関連付けられたオーバーロードが選択される。
auto変数宣言や関数戻り値の型において、制約のない型推論プレースホルダーの代わりに概念を使用することができます。
auto x1 = f ( y ); // x1 の型は f が返す型に推論されるSortable auto x2 = f ( y ); // x2 の型は推論されるが、Sortable を満たす場合にのみコンパイルされるISO/IEC TS 19217:2015 で規定されている TS の概念は、GCC 6で実験的な機能として実装されています。[ 7 ] C++20 の概念は、 GCC 10、[ 8 ] MSVC 19.30、[ 9 ]およびClang 10で完全に実装されています。[ 10 ]
概念の別の形式である「C++0x Concepts」[ 11 ]は、一時的にC++11のワーキング ペーパーに採用されましたが、2009 年に削除されました。[ 12 ]「C++0x Concepts」には、概念自体に加えて、概念マップStack(たとえば、概念がを受け入れることを可能にする機能で、 などの操作を の異なる名前の操作 ( など) に自動的にマッピングします)と公理(結合性や交換性などの意味的特性を指定する機能で、コンパイラが証明なしでこれらの特性を利用できるようにします) が含まれていました。提案された構文は次のようになります。std::vectorStackpush()std::vectorpush_back()
template < ValueType T > concept_map ForwardIterator < T *> { typedef T value_type ; };concept TotalOrder < typename Operator , typename T > { axiom Transitivity ( Operator op , T x , T y , T z ) { if ( op ( x , y ) && op ( y , z )) { op ( x , z ) <=> true ; } } }当初の提案は、コンパイル時の解決の曖昧さやコンパイル時動作の複雑化、そしてコンパイル中に公理を検証できないことから、最終的に放棄されました。この放棄された提案とは対照的に、Concepts の C++20 バージョンは「Concepts Lite」と呼ばれることがあります。[ 13 ]
2016年3月のC++標準委員会会議で、進化ワーキンググループはConceptsをメインラインのC++17標準に統合する動議を提出したが、この動議は委員会全体で否決された。[ 14 ]
Concepts v1 はC++20ドラフトに統合されました。[ 15 ]
概念に依存する Range 機能の「The One Range」バージョンもC++20に統合されました。
C#では、ジェネリック型の制約はwhere句で表現され、概念と同じくらい表現力がありますが、名前は付けられません。[ 16 ]
Systemを使用します。public class MyGenericClass < T , U > where T : IComparable < T > , allows ref struct where U : class , notnull , new () { // ... }Javaにはワイルドカードジェネリクスがあり、これは概念ほど表現力は高くないものの、型の境界を表すことができる。[ 17 ]
import java.util.List ; import java.util.stream.Collectors ;public static < T extends CharSequence & Comparable < T >> List < String > copyWhenGreater ( List < T > list , T threshold ) { return list . stream () . filter ( item -> item . compareTo ( threshold ) > 0 ) . map ( T :: toString ) . collect ( Collectors . toList ()); }Kotlin はJava スタイルの型ワイルドカードをサポートしていません。ただし、代わりに(たとえば、 )?として表現されます。それ以外は C# スタイルの句があります: [ 18 ]*List<*>where
fun < T > copyWhenGreater ( list : List < T > , threshold : T ): List < String > where T : CharSequence , T : Comparable < T > { return list . filter { it > threshold }. map { it . toString () } }Rustでは、トレイトを束縛するために句も使用されますwhere。
use std :: cmp :: Ord ;struct MyStruct < T > where T : Ord + Default , { value : T , }impl < T > MyStruct < T > where T : Ord + Default , { fn new ( value : T ) -> Self { MyStruct { value } }fn is_less_than ( & self , other : & Self ) -> bool { self . value < other . value } }