どんどん保存

静的にチェックされた投稿で、型特性ライブラリの関数は static_assert に最適であると書きました。その理由は、static_assert には定数式が必要だからです。型特性ライブラリの関数は、コンパイル時に実行できる多くのチェックを提供します。これらの投稿で、私の主張を証明します。

gcd - 最初

型特性ライブラリの機能を体系的に説明する前に、この投稿で例を示します。私の出発点は、2 つの数値の最大公約数を計算する Euclid アルゴリズムです。

アルゴリズムを関数テンプレートとして実装し、さまざまな引数を与えるのは非常に簡単です。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 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;
 std::cout << gcd("100","10") << std::endl;

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

 std::cout << std::endl;

}

しかし、プログラムのコンパイルは失敗します。コンパイラはテンプレートをインスタンス化しようとしますが無駄です。

関数テンプレートには 2 つの重大な問題があります。まず、あまりにも一般的です。したがって、関数テンプレートは double (行 21) と C 文字列 (行 22) を受け入れます。しかし、両方のデータ型の最大公約数を決定しても意味がありません。 double と C 文字列値のモジュロ演算は、9 行目で失敗します。しかし、それだけの問題ではありません。次に、gcd は 1 つの型パラメーター T に依存します。これは、関数テンプレート シグネチャ gcd(T a, T b)) を示しています。 a と b は同じ型 T である必要があります。型パラメーターの変換はありません。したがって、int 型と long 型を使用した gcd のインスタンス化 (24 行目) は失敗します。

型特性ライブラリのおかげで、最初の問題はすぐに解決されます。 2 番目の問題は、さらに多くの労力を必要とします。

gcd - 秒

簡単にするために、記事の残りの部分では、両方の引数が正の数でなければならないことを無視します。しかし、最初の問題に戻ります。 static_assert 演算子と述語 std::is_integral::value は、コンパイル時に T が整数型かどうかを確認するのに役立ちます。述語は常にブール値を返します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// gcd_2.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!");
 if( b == 0 ){ return a; }
 else{
 return gcd(b, a % b);
 }
}

int main(){

 std::cout << std::endl;

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

 std::cout << std::endl;

}

偉大な。 gcd アルゴリズムの最初の問題を解決しました。モジュロ演算子は double 値と C 文字列に対して定義されていないため、コンパイルが誤って失敗することはありません。 8 行目のアサーションが成り立たないため、コンパイルは失敗します。微妙な違いは、最初の例のように失敗したテンプレートのインスタンス化の暗号化された出力ではなく、正確なエラー メッセージが表示されることです。

ルールは非常に簡単です。 コンパイルは必ず失敗し、明確なエラー メッセージが表示されるはずです。

しかし、2番目の問題についてはどうですか。 gcd アルゴリズムは、異なる型の引数を受け入れる必要があります。

gcd - 3 番目

それは大したことではありません。しかし、やめてください。結果の型は何ですか?

1
2
3
4
5
6
7
8
9
template<typename T1, typename T2>
??? gcd(T1 a, T2 b){
 static_assert(std::is_integral<T1>::value, "T1 should be an integral type!");
 static_assert(std::is_integral<T2>::value, "T2 should be an integral type!");
 if( b == 0 ){ return a; }
 else{
 return gcd(b, a % b);
 }
}

2 行目の 3 つの疑問符は、問題の核心を示しています。最初の型と 2 番目の型のどちらをアルゴリズムの戻り値の型にする必要がありますか?それとも、アルゴリズムは 2 つの引数から新しい型を導出する必要がありますか?型特性ライブラリが私の助けになります。 2 つのバリエーションを紹介します。

小さいタイプ

戻り型の適切な選択は、両方の型の小さい方を使用することです。したがって、コンパイル時に三項演算子が必要です。私たちが持っている型特性ライブラリに感謝します。三項関数 std::conditional は、値ではなく型で動作します。これは、コンパイル時に関数を適用するためです。したがって、適切な定数式を std::conditional にフィードする必要があり、これで完了です。 std::conditional<(sizeof(T1) ::type は、T1 が T2 より小さい場合、コンパイル時に T1 を返します。 T1 が T1 より小さくなければ、T2 を返します。

ロジックを適用してみましょう。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// gcd_3_smaller.cpp

#include <iostream>
#include <type_traits>
#include <typeinfo>

template<typename T1, typename T2>
typename std::conditional <(sizeof(T1) < sizeof(T2)), T1, T2>::type gcd(T1 a, T2 b){
 static_assert(std::is_integral<T1>::value, "T1 should be an integral type!");
 static_assert(std::is_integral<T2>::value, "T2 should be an integral type!");
 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 << std::endl;

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

 std::conditional <(sizeof(100) < sizeof(10LL)), long long, long>::type uglyRes= gcd(100,10LL);
 auto res= gcd(100,10LL);
 auto res2= gcd(100LL,10L);

 std::cout << "typeid(gcd(100,10LL)).name(): " << typeid(res).name() << std::endl;
 std::cout << "typeid(gcd(100LL,10L)).name(): " << typeid(res2).name() << std::endl;

 std::cout << std::endl;

}

プログラムの重要な行は、gcd アルゴリズムの戻り値の型を持つ行 8 です。もちろん、このアルゴリズムは同じ型のテンプレート引数も処理できます。 21 行目から 24 行目とプログラムの出力で確認できます。しかし、27行目はどうですか? int 型の 100 と long long int 型の 10 を使用します。最大公約数の結果は 10 です。29 行目は非常に醜いです。式 std::conditional <(sizeof(100) ::type を繰り返して、変数uglyResの正しい型を決定する必要があります。 auto を使用した自動型推定が私の助けになります(30行目と31行目)。 33 行目と 34 行目の typeid 演算子は、int 型と long long int 型の引数の結果の型が int であることを示しています。; long long int 型と long int 型の結果の型が long int であること .

一般的なタイプ

次に、2 番目のバリエーションに進みます。多くの場合、コンパイル時に小さい方の型を決定する必要はありませんが、すべての型を暗黙的に変換できる型を決定する必要があります。これは、関数テンプレート std::common_type の仕事です。もちろん、すでにご存知のとおり、type-traits ライブラリです。 std::common_type は、任意の数のテンプレート引数を処理できます。もっと正式に言うと。 std::common_type は可変個引数テンプレートです。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// gcd_3_common.cpp

#include <iostream>
#include <type_traits>
#include <typeinfo>

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

int main(){

 std::cout << std::endl;

 std::cout << "typeid(gcd(100,10)).name(): " << typeid(gcd(100,10)).name() << std::endl;
 std::cout << "typeid(gcd(100,10L)).name(): " << typeid(gcd(100,10L)).name() << std::endl;
 std::cout << "typeid(gcd(100,10LL)).name(): " << typeid(gcd(100,10LL)).name() << std::endl;

 std::cout << std::endl;

}

最後の実装との唯一の違いは、8 行目の std::common_type が戻り値の型を決定することです。この例では、結果の型に関心があるため、gcd の結果を無視しています。引数の型 int と int int を使用すると int が得られます;引数の型が int および long int long int の場合; int と long long int long long int を使用 .


gcd - 4 番目

しかし、それだけではありません。 type-traits ライブラリの std::enable_if も非常に興味深いバリエーションを提供します。以前の実装には、引数が整数型である場合に関数本体でチェックするという共通点があります。重要な観察は、コンパイラが常に関数 Temple をインスタンス化しようとし、時々失敗することです。あなたは結果を知っています。 std::integral が返す式が失敗した場合、インスタンス化は失敗します。それは最善の方法ではありません。関数テンプレートが有効な型に対してのみ使用できるとよいでしょう。そのため、関数テンプレートのチェックをテンプレート本体からテンプレート署名まで入れています。

関数の引数に同じ型を使用した基本事項に集中するようにします。したがって、戻り値の型は明らかです。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// gcd_4.cpp

#include <iostream>
#include <type_traits>

template<typename T,
 typename std::enable_if<std::is_integral<T>::value,T>::type= 0> 
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(3.5,4)= " << gcd(3.5,4.0) << std::endl; 

 std::cout << std::endl;

}

行 7 は、新しいプログラムのキー行です。式 std::is_integral は、型パラメーター T が整数かどうかを決定します。 T が整数でないため、戻り値が false の場合、テンプレートのインスタンス化は取得されません。これは決定的な観察です。

std::enable_if が最初のパラメーターとして true を返す場合、std::enable_if は public メンバーの typedef 型を持ちます。この型は 7 行目で使用されます。std::enable_if が最初のパラメーターとして false を返す場合、std::enable_if には public メンバーの typedef 型がありません。したがって、7 行目は無効です。しかし、これはエラーではありません。まさにこのタイプのテンプレートのみがインスタンス化されません。

C++ の規則では、次のように述べられています。テンプレート パラメーターの推定型の置換が失敗すると、コンパイル エラーが発生する代わりに、オーバーロード セットから特殊化が破棄されます。このルールには短い頭字語 SFINAE (S) があります。 置換 F アリュール いいえ その他 A n E エラー).

コンパイルの出力はそれを示しています。 double 型のテンプレートの特殊化はありません。

次は?

型特性ライブラリに関する次の投稿は体系的に行われます。型特性ライブラリには多くの関数があります。これらを使用すると、コンパイル時に型を確認、比較、および変更できます。次の投稿で2つの質問に答えます。それはどのように機能しますか?と。利用できる機能は?