型特性ライブラリ:正しさ

型特性ライブラリの 2 つの主要な目標は、非常に説得力があります。それは、正確性と最適化です。今日は、正しさについて書きます。

型特性ライブラリを使用すると、コンパイル時にクエリ、型比較、および型変更を型付けできます。型特性ライブラリに関する以前の投稿では、型クエリと型比較についてのみ書きました。型特性ライブラリの正確さの側面について書く前に、型の変更について少し書きたいと思います。

型の変更

型特性ライブラリは、型を操作するための多くのメタ関数を提供します。ここに最も興味深いものがあります。

// const-volatile modifications:
remove_const;
remove_volatile;
remove_cv;
add_const;
add_volatile;
add_cv;

// reference modifications:
remove_reference;
add_lvalue_reference;
add_rvalue_reference;

// sign modifications:
make_signed;
make_unsigned;

// pointer modifications:
remove_pointer;
add_pointer;

// other transformations:
decay;
enable_if;
conditional;
common_type;
underlying_type;

int を取得するには int から または const int::type でタイプを要求する必要があります .

std::is_same<int, std::remove_const<int>::type>::value; // true
std::is_same<int, std::remove_const<const int>::type>::value; // true

C++14 以降、 _t を使用できます std::remove_const_t などで型を取得する :

std::is_same<int, std::remove_const_t<int>>::value; // true
std::is_same<int, std::remove_const_t<const int>>::value; // true

型特性ライブラリのこれらのメタ関数がどれほど有用であるかを理解するために、いくつかの例を示します。

  • std::decay : std::thread 適用 std::decay その引数に。 std::thread の引数 実行された関数を含む f とその引数 args .減衰とは、配列からポインターへ、関数からポインターへの暗黙的な変換が実行され、 const/volatile が実行されることを意味します。 修飾子と参照は削除されます。
  • std::enable_if SFINAE を使用する便利な方法です。 SFINAE は Substitution Failure Is Not An Error の略で、関数テンプレートのオーバーロード解決中に適用されます。これは、テンプレート パラメーターの置換が失敗した場合、特殊化はオーバーロード セットから破棄されますが、この失敗によってコンパイラ エラーが発生しないことを意味します。
  • std::conditional コンパイル時の三項演算子です。
  • std::common_type すべての型を変換できるすべての型の中で共通の型を決定します。
  • std::underlying_type 列挙型の型を決定します。

おそらく、型特性ライブラリの利点について確信が持てないでしょう。型特性ライブラリの 2 つの主要な目標である正確性と最適化について、私の一連の投稿を終了させて​​ください。

正確さ

正確性とは、C++11 の型特性ライブラリを使用して、アルゴリズムをより安全にすることができることを意味します。次の gcd アルゴリズムの実装では、引数に対してバイナリ モジュロ演算子が有効である必要があります。

// gcd2.cpp

#include <iostream>
#include <type_traits>

template<typename T>
T gcd(T a, T b) {
 static_assert(std::is_integral<T>::value, "T should be an integral type!"); // (1)
 if( b == 0 ){ return a; }
 else{
 return gcd(b, a % b);
 }
}

int main() {

 std::cout << gcd(100, 33) << '\n';
 std::cout << gcd(3.5,4.0) << '\n';
 std::cout << gcd("100","10") << '\n';

}

エラー メッセージは非常に明確です。

コンパイラは、 double であるとすぐに文句を言います。 または const cha r* は整数データ型ではありません。その結果、static_assert (1) 発火の式

ただし、正確さは、型特性ライブラリを使用して Integral などの概念を実装できることを意味します 、 SignedIntegral 、および UnsignedIntegral C++20 で。

template <typename T>
concept Integral = std::is_integral<T>::value; // (1)

template <typename T>
concept SignedIntegral = Integral<T> && std::is_signed<T>::value; // (2)

template <typename T>
concept UnsignedIntegral = Integral<T> && !SignedIntegral<T>;

コンセプト Integral 型特性関数 std::is_integral を直接使用します (1) とコンセプト SignedIntegral 型特性関数 std::is_signed (2).

Integral という概念を使って試してみましょう

// gcdIntegral.cpp

#include <iostream>
#include <type_traits>

template <typename T>
concept Integral = std::is_integral<T>::value;

template <typename T>
concept SignedIntegral = Integral<T> && std::is_signed<T>::value;

template <typename T>
concept UnsignedIntegral = Integral<T> && !SignedIntegral<T>;

Integral auto gcd(Integral auto a, decltype(a) b) {
 if( b == 0 ){ return a; }
 else{
 return gcd(b, a % b);
 }
}

int main() {

 std::cout << gcd(100, 33) << '\n';
 std::cout << gcd(3.5,4.0) << '\n';
 std::cout << gcd("100","10") << '\n';

}

これで、gcd アルゴリズムが読みやすくなりました。最初の引数 a が必要です およびその戻り値の型は整数データ型です。確実にするために、2 番目の引数 b 最初のタイプ a などの同じタイプを持っています 、そのタイプを decltype(a) と指定しました .結果として、この gcd の実装 アルゴリズムと gcd2.cpの前のアルゴリズム p は同等です。

現在、エラー メッセージは以前のもののようにより詳細です。

GCC のエラー メッセージは冗長すぎるだけでなく、読みにくいものでもあります。 Compiler Explorer で Clang を試してみましょう。散文のような二重読み取りの誤った使用に関するエラー メッセージ:

正直なところ、これほど読みやすいエラー メッセージはないと思います。

最後に、最新の Microsoft Visual Studio Compiler を試してみたいと思いました。このコンパイラは、1 つの例外を除いて概念をサポートしています。いわゆる省略関数テンプレート構文です。あなたはすでにそれを推測しているかもしれません。 gcd アルゴリズムでは、短縮された関数テンプレート構文を使用しました。この優れた構文の詳細については、私の以前の投稿「C++20:概念 - シンタクティック シュガー」を参照してください。

次は?

もちろん、次の投稿で何を書こうとしているのかはご存知でしょう:型特性ライブラリのパフォーマンスの話です。