C++20 によるさまざまなテンプレートの改善

確かに、この投稿では、テンプレートと C++20 全般に対するいくつかの小さな改善点を紹介します。これらの改善はそれほど印象的ではないように思えるかもしれませんが、C++20 の一貫性が向上するため、一般的なプログラムを作成する際にエラーが発生しにくくなります。

今日の投稿は、条件付きで明示的なコンストラクターと新しい非型テンプレート パラメーターについてです。

条件付き明示コンストラクタ

さまざまな型を受け入れるコンストラクタを持つクラスが必要な場合があります。たとえば、さまざまな型を受け入れる std::variant を保持するクラス VariantWrapper があるとします。

class VariantWrapper {

 std::variant<bool, char, int, double, float, std::string> myVariant;

};

myVariant を bool、char、int、double、float、または std::string で初期化するには、クラス VariantWrapper にリストされている各型のコンストラクターが必要です。怠惰は (少なくともプログラマーにとって) 美徳であるため、コンストラクターをジェネリックにすることにします。

クラス Implicit は、ジェネリック コンストラクターの例です。

// explicitBool.cpp

#include <iostream>
#include <string>
#include <type_traits>

struct Implicit {
 template <typename T> // (1)
 Implicit(T t) {
 std::cout << t << std::endl;
 }
};

struct Explicit {
 template <typename T>
 explicit Explicit(T t) { // (2)
 std::cout << t << std::endl;
 }
};

int main() {
 
 std::cout << std::endl;
 
 Implicit imp1 = "implicit";
 Implicit imp2("explicit");
 Implicit imp3 = 1998;
 Implicit imp4(1998);
 
 std::cout << std::endl;
 
 // Explicit exp1 = "implicit"; // (3)
 Explicit exp2{"explicit"}; // (4)
 // Explicit exp3 = 2011; // (3)
 Explicit exp4{2011}; // (4)
 
 std::cout << std::endl; 

} 

さて、問題があります。ジェネリック コンストラクター (1) は、任意の型で呼び出すことができるため、すべてをキャッチするコンストラクターです。コンストラクターはあまりにも貪欲です。コンストラクターの前に明示的に配置する (2)。コンストラクターは明示的になります。これは、暗黙の変換 (3) が無効になったことを意味します。明示的な呼び出し (4) のみが有効です。

Clang 10 のおかげで、プログラムの出力は次のようになります:

これは話の最後ではありません。おそらく、bool からの暗黙的な変換のみをサポートし、他の暗黙的な変換をサポートしない MyBool 型があるとします。この場合、条件付きで明示的に使用できます。

// myBool.cpp

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

struct MyBool {
 template <typename T>
 explicit(!std::is_same<T, bool>::value) MyBool(T t) { // (1)
 std::cout << typeid(t).name() << std::endl;
 }
};

void needBool(MyBool b){ } // (2)

int main() {

 MyBool myBool1(true); 
 MyBool myBool2 = false; // (3)
 
 needBool(myBool1);
 needBool(true); // (4)
 // needBool(5);
 // needBool("true");
 
}

explicit(!std::is_same::value) 式は、MyBool が bool 値から暗黙的にのみ作成できることを保証します。関数 std::is_same は、type_traits ライブラリのコンパイル時の述語です。コンパイル時の述語は、 std::is_same がコンパイル時に評価され、ブール値を返すことを意味します。したがって、(3) と (4) の bool からの暗黙的な変換は可能ですが、int と C 文字列からのコメントアウトされた変換はできません。

条件付きで明示的なコンストラクターが SFINAE で可能であると主張するとき、あなたは正しいです。しかし、正直なところ、コンストラクターを使用した対応する SFINAE は好きではありません。説明するには数行かかるからです。さらに、3 回目の試行の直後にしか取得できません。

template <typename T, std::enable_if_t<std::is_same_v<std::decay_t<T>, bool>, bool> = true>
MyBool(T&& t) {
 std::cout << typeid(t).name() << std::endl;
}

いくつかの説明的な言葉を追加する必要があると思います。 std::enable_if は、SFINAE を使用する便利な方法です。 SFINAE は S の略です 置換 F アリュール いいえ その他 A n E エラーが発生し、関数テンプレートのオーバーロードの解決中に適用されます。これは、テンプレート パラメーターの置換が失敗した場合、特殊化はオーバーロード セットから破棄されますが、コンパイラ エラーは発生しないことを意味します。これはまさにこの具体的なケースで起こります。 std::is_same_v, bool> が false と評価された場合、特殊化は破棄されます。 std::decay は T からの const、volatile、または参照の削除などの変換を T に適用します。 std::decay_t は std::decay::type の便利な構文です。同じことが std::is_same_v にも当てはまります。これは std::is_same::value の略です。

プレ アルファのドイツ人読者が指摘したように、SFINAE を使用するコンストラクタは欲張りすぎます。非ブール コンストラクタをすべて無効にします。

私の長い説明のほかに、SFINAE に反対し、条件付きで明示的なコンストラクターを支持する追加の議論があります:パフォーマンスです。 Simon Brand は、彼の投稿「C++20 の条件付き明示的コンストラクター」で、explicit(bool) が Visual Studio 2019 のテンプレートのインスタンス化を SFINAE と比較して約 15% 高速化したと指摘しました。

C++20 では、追加の非型テンプレート パラメータがサポートされています。

新しい非型テンプレート パラメータ

C++20 では、constexpr コンストラクターを持つ浮動小数点とクラスは非型としてサポートされます。

C++ は、非型をテンプレート パラメーターとしてサポートします。基本的に非型は

  • 整数と列挙子
  • オブジェクト、関数、およびクラスの属性へのポインタまたは参照
  • std::nullptr_t

クラスの生徒に、型以外のパラメーターをテンプレート パラメーターとして使用したことがあるかどうかを尋ねると、彼らは次のように答えます。もちろん、私自身のトリッキーな質問に答えて、非型テンプレート パラメータのよく使用される例を示します。

std::array<int, 5> myVec;

5 は非型であり、テンプレート引数として使用されます。私たちはそれに慣れているだけです。最初の C++ 標準 C++98 以降、テンプレート パラメーターとして浮動小数点をサポートする議論が C++ コミュニティで行われています。これで、C++20 が完成しました:

// nonTypeTemplateParameter.cpp

struct ClassType {
 constexpr ClassType(int) {} // (1)
};

template <ClassType cl> // (2)
auto getClassType() {
 return cl;
}

template <double d> // (3)
auto getDouble() {
 return d;
}

int main() {

 auto c1 = getClassType<ClassType(2020)>();

 auto d1 = getDouble<5.5>(); // (4)
 auto d2 = getDouble<6.5>(); // (4)

}

ClassType には constexpr コンストラクター (1) があるため、テンプレート引数 (2) として使用できます。同じことが、double のみを受け入れる関数テンプレート getDouble (3) にも当てはまります。関数テンプレート getDouble (4) を新しい引数で呼び出すたびに、新しい関数 getDouble のインスタンス化がトリガーされることを強調したいと思います。これは、double 5.5 と 6.5 の 2 つのインスタンス化が作成されることを意味します。

Clang が既にこの機能をサポートしている場合は、5.5 および 6.5 のインスタンス化ごとに完全に特殊化された関数テンプレートが作成されることを C++ Insights で示すことができます。少なくとも、GCC のおかげで、関連するアセンブラー命令を Compiler Explorer で表示できます。

スクリーンショットは、コンパイラがテンプレート引数ごとに関数を作成したことを示しています。

次は?

テンプレートとして、ラムダも C++20 でさまざまな方法で改善されています。私の次の投稿は、これらのさまざまな改善についてです。