C++ コア ガイドライン:Type-Traits を使用したコンパイル時のプログラミング (その 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 または const int から int を取得するには、::type で型を要求する必要があります。

int main(){
 
 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 以降、std::remove_const_t:のように _t を使用して型を取得できます:

int main(){
 
 std::is_same<int, std::remove_const_t<int>>::value; // true
 std::is_same<int, std::remove_const_t<const int>>::value; // true
}

型特性ライブラリのこれらのメタ関数がどれほど役立つかを理解するために、いくつかの使用例を示します。ここに std::move を 1 行で示します。

  • remove_reference :std::move および std::forward はこの関数を使用して、その引数から参照を削除します。
    • static_cast<std::remove_reference<decltype(arg)>::type&&>(arg);
  • 崩壊: std::thread は std::decay をその引数に適用します。それらの使用法には、スレッドがその引数 args で実行する関数 f が含まれます。減衰とは、配列からポインター、関数からポインターへの暗黙的な変換が実行され、const/volatile 修飾子と参照が削除されることを意味します。
    • std::invoke(decay_copy(std::forward<Function>(f)), 
       decay_copy(std::forward<Args>(args))...);
  • enable_if :std::enable_if は、SFINAE を使用する便利な方法です。 SFINAE は S の略です 置換 F アリュール いいえ その他 A n E エラーが発生し、関数テンプレートのオーバーロードの解決中に適用されます。これは、テンプレート パラメーターの置換が失敗した場合、特殊化はオーバーロード セットから破棄されますが、コンパイラ エラーは発生しないことを意味します。 std::enable_if は std::tuple で頻繁に使用されます。
  • 条件付き :std::conditional はコンパイル時の三項演算子です。
  • common_type: std::common_type は、型のグループの共通の型を決定します。
  • underlying_type: std::underlying_type は列挙型の型を決定します。

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

正確さ

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

template <class T>
concept bool Integral() {
 return is_integral<T>::value;
}

template <class T>
concept bool SignedIntegral() {
 return Integral<T>() && is_signed<T>::value;
}

template <class T>
concept bool UnsignedIntegral() {
 return Integral<T>() && !SignedIntegral<T>();
}

しかし、それはまた、それらを使用してアルゴリズムをより安全にすることができることも意味します.前回の投稿でより安全に、型特性ライブラリの関数 std::is_integral、std::conditional、std::common_type、および std::enable_if を使用して、ジェネリック gcd アルゴリズムを連続的に安全にしました。

More and More Safe の投稿をよりよく理解するために、ここに私の一般的な gcd アルゴリズムの出発点を示します。

// gcd.cpp

#include <iostream>

template<typename T>
T gcd(T a, T b){
 if( b == 0 ){ return a; }
 else{
 return gcd(b, a % b);
 }
}

int main(){

 std::cout << std::endl;

 std::cout << "gcd(100, 10)= " << gcd(100, 10) << std::endl;
 std::cout << "gcd(100, 33)= " << gcd(100, 33) << std::endl;
 std::cout << "gcd(100, 0)= " << gcd(100, 0) << std::endl;

 std::cout << gcd(3.5, 4.0)<< std::endl; // (1)
 std::cout << gcd("100", "10") << std::endl; // (2)

 std::cout << gcd(100, 10L) << std::endl; // (3)

 std::cout << std::endl;

}

プログラムの出力は、2 つの問題を示しています。

まず、モジュロ演算子で double (行 1) と C-String (行 2) を使用すると失敗します。次に、integer と long の使用 (3 行目) が機能するはずです。どちらの問題も、type-traits ライブラリを使用してエレガントに解決できます。

型特性は正確さだけでなく、最適化にも関係します。

最適化

型特性ライブラリの重要なアイデアは非常に単純です。コンパイラは使用されている型を分析し、この分析に基づいて実行するコードを決定します。標準テンプレート ライブラリのアルゴリズム std::copy、std::fill、または std::equal の場合、これは、アルゴリズムが範囲の各要素に 1 つずつ、またはメモリ全体に適用されることを意味します。 2 番目のケースでは、アルゴリズムを高速化する memcmp、memset、memcpy、memmove などの C 関数が使用されます。 memcpy と memmove の小さな違いは、memmove が重複するメモリ領域を処理できることです。

GCC 6 実装からの次の 3 つのコード スニペットは、1 つのポイントを明確にします:型特性ライブラリのチェックは、より最適化されたコードを生成するのに役立ちます。

// fill 
// Specialization: for char types we can use memset. 
template<typename _Tp>
 inline typename
 __gnu_cxx::__enable_if<__is_byte<_Tp>::__value, void>::__type // (1)
 __fill_a(_Tp* __first, _Tp* __last, const _Tp& __c)
 {
 const _Tp __tmp = __c;
 if (const size_t __len = __last - __first)
 __builtin_memset(__first, static_cast<unsigned char>(__tmp), __len);
 }

// copy

template<bool _IsMove, typename _II, typename _OI>
 inline _OI
 __copy_move_a(_II __first, _II __last, _OI __result)
 {
 typedef typename iterator_traits<_II>::value_type _ValueTypeI;
 typedef typename iterator_traits<_OI>::value_type _ValueTypeO;
 typedef typename iterator_traits<_II>::iterator_category _Category;
 const bool __simple = (__is_trivial(_ValueTypeI) // (2)
 && __is_pointer<_II>::__value
 && __is_pointer<_OI>::__value
 && __are_same<_ValueTypeI, _ValueTypeO>::__value);

 return std::__copy_move<_IsMove, __simple,
 }

// lexicographical_compare

template<typename _II1, typename _II2>
 inline bool
 __lexicographical_compare_aux(_II1 __first1, _II1 __last1,
 _II2 __first2, _II2 __last2)
 {
 typedef typename iterator_traits<_II1>::value_type _ValueType1;
 typedef typename iterator_traits<_II2>::value_type _ValueType2;
 const bool __simple = // (3)
 (__is_byte<_ValueType1>::__value && __is_byte<_ValueType2>::__value
 && !__gnu_cxx::__numeric_traits<_ValueType1>::__is_signed
 && !__gnu_cxx::__numeric_traits<_ValueType2>::__is_signed
 && __is_pointer<_II1>::__value
 && __is_pointer<_II2>::__value);

 return std::__lexicographical_compare<__simple>::__lc(__first1, __last1,
 __first2, __last2);
 }

行 1、2、および 3 は、型特性ライブラリを使用して、より最適化されたコードを生成することを示しています。私の投稿 Type-Traits:Performance Matters は、より多くの洞察を提供し、GCC と MSVC でのパフォーマンス数値を示しています。

次は?

コンパイル時の constexpr プログラミングは、その専門家のニッチを回避し、主流の手法になります。 constexpr は、典型的な C++ 構文を使用してコンパイル時にプログラミングします。