C++ コア ガイドライン:概念の定義、その 2

ガイドラインで概念を定義するためのルールを続けます。この投稿では、残りの 3 つのルールのうち最初のルールは非常に洗練されています。

今日のルールは次のとおりです。

  • T.24:タグ クラスまたは特性を使用して、セマンティクスのみが異なる概念を区別する
  • T.25:補完的な制約を避ける
  • T.26:単純な構文よりも使用パターンの観点から概念を定義することを好む

最初のルールの説明は非常に簡潔です。簡潔すぎるかもしれません。

T.24:タグ クラスまたは特性を使用して概念を区別するセマンティクスのみが異なる

これが、ガイドラインのこの規則の理由です:「同じ構文を必要とするが異なるセマンティクスを持つ 2 つの概念は、プログラマーがそれらを区別しない限り、あいまいさにつながります。」

仮定しましょう。 is_contiguous トレイトを定義しました。この場合、ランダム アクセス イテレータ RA_iter と連続イテレータ Contiguous_iter を区別するために使用できます。

template<typename I> // iterator providing random access
concept bool RA_iter = ...;

template<typename I> // iterator providing random access to contiguous data
concept bool Contiguous_iter =
 RA_iter<I> && is_contiguous<I>::value; // using is_contiguous trait

is_contiguous などのタグ クラスを概念にラップして使用することもできます。これで、アイデアの連続反復子 Contiguous_iter のより直接的な表現が得られました。

template<typename I> concept Contiguous = is_contiguous<I>::value;

template<typename I>
concept bool Contiguous_iter = RA_iter<I> && Contiguous<I>;

では、まず、特性とタグ ディスパッチという 2 つの重要な用語について説明します。

特性

特性は、ジェネリック型からプロパティを抽出するクラス テンプレートです。

次のプログラムは、型特性ライブラリの 14 の主要な型カテゴリのそれぞれについて、特定の特性を満たす型を提示します。プライマリ タイプ カテゴリは完全であり、重複していません。したがって、各タイプはタイプ カテゴリのメンバーです。タイプのタイプ カテゴリをチェックすると、リクエストは const または volatile 修飾子に依存しません。

// traitsPrimary.cpp

#include <iostream>
#include <type_traits>

using namespace std;

template <typename T>
void getPrimaryTypeCategory(){

 cout << boolalpha << endl;

 cout << "is_void<T>::value: " << is_void<T>::value << endl;
 cout << "is_integral<T>::value: " << is_integral<T>::value << endl;
 cout << "is_floating_point<T>::value: " << is_floating_point<T>::value << endl;
 cout << "is_array<T>::value: " << is_array<T>::value << endl;
 cout << "is_pointer<T>::value: " << is_pointer<T>::value << endl;
 cout << "is_null_pointer<T>::value: " << is_null_pointer<T>::value << endl;
 cout << "is_member_object_pointer<T>::value: " << is_member_object_pointer<T>::value << endl;
 cout << "is_member_function_pointer<T>::value: " << is_member_function_pointer<T>::value << endl;
 cout << "is_enum<T>::value: " << is_enum<T>::value << endl;
 cout << "is_union<T>::value: " << is_union<T>::value << endl;
 cout << "is_class<T>::value: " << is_class<T>::value << endl;
 cout << "is_function<T>::value: " << is_function<T>::value << endl;
 cout << "is_lvalue_reference<T>::value: " << is_lvalue_reference<T>::value << endl;
 cout << "is_rvalue_reference<T>::value: " << is_rvalue_reference<T>::value << endl;

 cout << endl;

}

int main(){
 
 getPrimaryTypeCategory<void>(); // (1)
 getPrimaryTypeCategory<short>(); // (1)
 getPrimaryTypeCategory<double>();
 getPrimaryTypeCategory<int []>();
 getPrimaryTypeCategory<int*>();
 getPrimaryTypeCategory<std::nullptr_t>();
 struct A{
 int a;
 int f(double){return 2011;}
 };
 getPrimaryTypeCategory<int A::*>();
 getPrimaryTypeCategory<int (A::*)(double)>();
 enum E{
 e= 1,
 };
 getPrimaryTypeCategory<E>();
 union U{
 int u;
 };
 getPrimaryTypeCategory<U>();
 getPrimaryTypeCategory<string>();
 getPrimaryTypeCategory<int * (double)>();
 getPrimaryTypeCategory<int&>(); // (2) 
 getPrimaryTypeCategory<int&&>(); // (2)
 
}

私はあなたを死ぬほど退屈させたくありません。したがって、行 (1) の出力のみがあります。

これが行 (2) の出力です。

タグのディスパッチ

タグのディスパッチにより、型のプロパティに基づいて関数を選択できます。決定はコンパイル時に行われ、前の段落で説明した特性が使用されます。

タグ ディスパッチの典型的な例は、標準テンプレート ライブラリの std::advance アルゴリズムです。 std::advance(it, n) は、反復子 it を n 要素だけインクリメントします。プログラムは重要なアイデアを示します。

// advanceTagDispatch.cpp

#include <iterator>
#include <forward_list>
#include <list>
#include <vector>
#include <iostream>

template <typename InputIterator, typename Distance>
void advance_impl(InputIterator& i, Distance n, std::input_iterator_tag) {
 std::cout << "InputIterator used" << std::endl; 
 while (n--) ++i;
}

template <typename BidirectionalIterator, typename Distance>
void advance_impl(BidirectionalIterator& i, Distance n, std::bidirectional_iterator_tag) {
 std::cout << "BidirectionalIterator used" << std::endl;
 if (n >= 0) 
 while (n--) ++i;
 else 
 while (n++) --i;
}

template <typename RandomAccessIterator, typename Distance>
void advance_impl(RandomAccessIterator& i, Distance n, std::random_access_iterator_tag) {
 std::cout << "RandomAccessIterator used" << std::endl;
 i += n;
}

template <typename InputIterator, typename Distance>
void advance_(InputIterator& i, Distance n) {
 typename std::iterator_traits<InputIterator>::iterator_category category; // (1)
 advance_impl(i, n, category); // (2)
}
 
int main(){
 
 std::cout << std::endl;
 
 std::vector<int> myVec{0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
 auto myVecIt = myVec.begin(); // (3)
 std::cout << "*myVecIt: " << *myVecIt << std::endl;
 advance_(myVecIt, 5);
 std::cout << "*myVecIt: " << *myVecIt << std::endl;
 
 std::cout << std::endl;
 
 std::list<int> myList{0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
 auto myListIt = myList.begin(); // (4)
 std::cout << "*myListIt: " << *myListIt << std::endl;
 advance_(myListIt, 5);
 std::cout << "*myListIt: " << *myListIt << std::endl;
 
 std::cout << std::endl;
 
 std::forward_list<int> myForwardList{0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
 auto myForwardListIt = myForwardList.begin(); // (5)
 std::cout << "*myForwardListIt: " << *myForwardListIt << std::endl;
 advance_(myForwardListIt, 5);
 std::cout << "*myForwardListIt: " << *myForwardListIt << std::endl;
 
 std::cout << std::endl;
 
}

式 std::iterator_traits::iterator_category カテゴリは、コンパイル時にイテレータ カテゴリを決定します。イテレータ カテゴリに基づいて、関数 Advance_impl(i, n, category) の最も具体的な変数が行 (2) で使用されます。各コンテナは、その構造に対応するイテレータ カテゴリのイテレータを返します。したがって、行 (3) はランダム アクセス反復子を提供し、行 (4) は双方向反復子を提供し、行 (5) は入力反復子でもある前方反復子を提供します。

パフォーマンスの観点からすると、この区別は非常に理にかなっています。双方向イテレータよりも高速にインクリメントでき、双方向イテレータは入力イテレータよりも高速にインクリメントできます。ユーザーの観点から見ると、std::advance(it, 5) を呼び出すと、コンテナーが満たす最速のバージョンが得られます。

これはかなり冗長でした。残りの 2 つのルールに追加することはあまりありません。

T.25:補完的な制約を避ける

ガイドラインの例は、補完的な制約を示しています。

template<typename T> 
 requires !C<T> // bad 
void f(); 

template<typename T> 
 requires C<T> 
void f();


避けてください。代わりに、制約のないテンプレートと制約のあるテンプレートを作成してください。

template<typename T> // general template
 void f();

template<typename T> // specialization by concept
 requires C<T>
void f();

制約されたバージョンのみが使用されるように、制約のないバージョンを削除するように設定することもできます。

template<typename T>
void f() = delete;

T.26:使用に関して概念を定義することを好む単純な構文ではなくパターン

このガイドラインのタイトルはかなりあいまいですが、例を見れば一目瞭然です。

概念 has_equal と has_not_equal を使用して概念 Equality を定義する代わりに

template<typename T> concept Equality = has_equal<T> && has_not_equal<T>;

使用パターンを使用します。これは以前のバージョンより読みやすくなっています:

template<typename T> concept Equality = requires(T a, T b) {
 bool == { a == b }
 bool == { a != b }
 // axiom { !(a == b) == (a != b) }
 // axiom { a = b; => a == b } // => means "implies"
}

この場合、等価の概念では、==と !=を引数に適用でき、両方の演算が bool を返す必要があります。

次は?

以下は、C++ コア ガイドラインからテンプレート インターフェイスへの開始部分の一部です。お分かりのように、次の投稿は重要です。