関数テンプレートの完全な特殊化

前回の投稿テンプレートの特殊化からわかるように、関数テンプレートは完全にしか特殊化できませんが、部分的に特殊化することはできません。長い話を簡単に言うと、関数テンプレートを特殊化しないでください。関数のオーバーロードを使用するだけです。

なぜ私が C++ の機能について書いているのか疑問に思われるかもしれませんが、使用すべきではありません。理由は非常に簡単です。完全に特殊化された関数テンプレートの驚くべき動作が見られた場合は、代わりに非ジェネリック関数を使用することをお勧めします.

関数テンプレートを特殊化しない

タイトルで思い出すかも?右。これは C++ コア ガイドラインからのタイトルです:T.144:関数テンプレートを特殊化しない

ルールの理由は非常に短く、関数テンプレートの特殊化はオーバーロードに参加しません。それが何を意味するか見てみましょう。私のプログラムは、Dimov/Abrahams のプログラム スニペットに基づいています。

// dimovAbrahams.cpp

#include <iostream>
#include <string>

// getTypeName

template<typename T> // (1) primary template
std::string getTypeName(T){
 return "unknown";
}

template<typename T> // (2) primary template that overloads (1)
std::string getTypeName(T*){
 return "pointer";
}

template<> // (3) explicit specialization of (2)
std::string getTypeName(int*){
 return "int pointer";
}

// getTypeName2

template<typename T> // (4) primary template
std::string getTypeName2(T){
 return "unknown";
}

template<> // (5) explicit specialization of (4)
std::string getTypeName2(int*){
 return "int pointer";
}

template<typename T> // (6) primary template that overloads (4)
std::string getTypeName2(T*){
 return "pointer";
}

int main(){
 
 std::cout << '\n';
 
 int* p;
 
 std::cout << "getTypeName(p): " << getTypeName(p) << '\n'; 
 std::cout << "getTypeName2(p): " << getTypeName2(p) << '\n';
 
 std::cout << '\n';
 
}

確かに、コードは非常に退屈に見えますが、我慢してください。プライマリ テンプレート getTypeName をインライン (1) で定義しました。 (2) はポインターのオーバーロードであり、(3) int の完全な特殊化です。 ポインター。 getTypeName2 の場合は、小さなバリエーションを作成しました。明示的な specialisation を入れました (5) ポインターのオーバーロードの前 (6)。

この並べ替えは驚くべき結果をもたらします。

最初のケースでは int ポインターの完全な特殊化が呼び出され、2 番目のケースではポインターのオーバーロードが呼び出されます。何?この非直感的な動作の理由は、オーバーロードの解決が関数テンプレートの特殊化を無視するためです。オーバーロードの解決は、プライマリ テンプレートと関数で動作します。どちらの場合も、オーバーロードの解決により、両方のプライマリ テンプレートが見つかりました。最初のケース (getTypeName) では、ポインター バリアントの方が適しているため、int ポインターの明示的な特殊化が選択されました。 2 番目のバリアント (getTypeName2) では、ポインター バリアントが選択されていますが、完全な特殊化はプライマリ テンプレートに属しています (4 行目)。その結果、無視されました。

これはかなり複雑でした。次のルールに注意してください: 関数テンプレートを特殊化せず、代わりに非汎用関数を使用してください。

私の発言の証拠が欲しいですか?ここにあります:(3) と (5) の非ジェネリック関数の明示的な特殊化を作成すると、問題が解決します。テンプレート宣言 template<> をコメントアウトするだけです .簡単にするために、他のコメントは削除しました。

// dimovAbrahams.cpp

#include <iostream>
#include <string>

// getTypeName

template<typename T> 
std::string getTypeName(T){
 return "unknown";
}

template<typename T> 
std::string getTypeName(T*){
 return "pointer";
}

// template<> // (3) std::string getTypeName(int*){ return "int pointer"; } // getTypeName2 template<typename T> std::string getTypeName2(T){ return "unknown"; }
// template<> // (5) std::string getTypeName2(int*){ return "int pointer"; } template<typename T> std::string getTypeName2(T*){ return "pointer"; } int main(){ std::cout << '\n'; int* p; std::cout << "getTypeName(p): " << getTypeName(p) << '\n'; std::cout << "getTypeName2(p): " << getTypeName2(p) << '\n'; std::cout << '\n'; }

現在、関数のオーバーロードは期待どおりに機能し、非ジェネリック関数は int を受け取ります ポインターが使用されます。

テンプレート引数についてはすでに書きました。しかし、一つ重要な事実を忘れていました。関数テンプレートとクラス テンプレートにデフォルトのテンプレート引数を指定できます。

デフォルトのテンプレート引数

標準テンプレート ライブラリ (STL) のクラス テンプレートに共通するものは何ですか?はい!テンプレート引数の多くにはデフォルトがあります。

以下にいくつかの例を示します。

template<
 typename T,
 typename Allocator = std::allocator<T>
> class vector;

template<
 typename Key,
 typename T,
 typename Hash = std::hash<Key>,
 typename KeyEqual = std::equal_to<Key>,
 typename Allocator = std::allocator< std::pair<const Key, T>>
> class unordered_map;

template<
 typename T,
 typename Allocator = std::allocator<T>
> class deque;

template<
 typename T,
 typename Container = std::deque<T>
> class stack;

template<
 typename CharT,
 typename Traits = std::char_traits<CharT>,
 typename Allocator = std::allocator<CharT>
> class basic_string;

これは STL の機能の一部です:

  • 各コンテナには、その要素に依存するデフォルトのアロケータがあります。
  • std::unordered_map: std::unordered_map<std::string, int>.
    のキー タイプや値タイプなど、必要な引数を指定する必要があります。
  • <リ>

    std::unordered_map をインスタンス化できます キーの has 値を返す特別なハッシュ関数と、2 つのキーが等しいかどうかを判断する特別なバイナリ述語を使用する:std::unordered_map<std::string, int, MyHash> 、または std::unordered_map<std::string, int, MyHash, MyBinaryPredicate> .

  • std::string は、一般的な文字型の単なるエイリアスです。 std::basic_string. に基づくエイリアスは次のとおりです。
std::string std::basic_string<char>
std::wstring std::basic_string<wchar_t>
std::u8string std::basic_string<char8_t> (C++20)
std::u16string std::basic_string<char16_t> (C++11)
std::u32string std::basic_string<char32_t> (C++11)

もちろん、テンプレート引数にデフォルトがある場合、次のテンプレート引数にもデフォルトが必要です。

これまでは、クラス テンプレートの既定のテンプレート引数についてのみ説明してきました。関数テンプレートに関する例を挙げて、この記事を締めくくりたいと思います。

同じタイプのいくつかのオブジェクトのうち、どちらが小さいかを判断したいとします。 isSmaller などのアルゴリズム 一般的なアイデアをモデル化しているため、テンプレートにする必要があります。

// templateDefaultArguments.cpp

#include <functional>
#include <iostream>
#include <string>

class Account{
public:
 explicit Account(double b): balance(b){}
 double getBalance() const {
 return balance;
 }
private:
 double balance;
};

template <typename T, typename Pred = std::less<T>> // (1)
bool isSmaller(T fir, T sec, Pred pred = Pred() ){
 return pred(fir,sec);
}

int main(){

 std::cout << std::boolalpha << '\n';

 std::cout << "isSmaller(3,4): " << isSmaller(3,4) << '\n'; // (2) 
 std::cout << "isSmaller(2.14,3.14): " << isSmaller(2.14,3.14) << '\n';
 std::cout << "isSmaller(std::string(abc),std::string(def)): " << 
 isSmaller(std::string("abc"),std::string("def")) << '\n';

 bool resAcc= isSmaller(Account(100.0),Account(200.0), // (3)
 [](const Account& fir, const Account& sec){ return fir.getBalance() < sec.getBalance(); });
 std::cout << "isSmaller(Account(100.0),Account(200.0)): " << resAcc << '\n';

 bool acc= isSmaller(std::string("3.14"),std::string("2.14"), // (4)
 [](const std::string& fir, const std::string& sec){ return std::stod(fir) < std::stod(sec); });
 std::cout << "isSmaller(std::string(3.14),std::string(2.14)): " << acc << '\n';

 std::cout << '\n';

}

デフォルトのケース (2) では、 isSmaller 期待どおりに動作します。 isSmaller (1) テンプレート引数 std::less を使用 これは、STL で定義済みの多くの関数オブジェクトの 1 つです。小なり演算子 < を適用します その引数に。これを使用するには、次の行で std::less をインスタンス化する必要がありました: Pred pred = Pred() .

デフォルトのテンプレート引数のおかげで、アカウント (3) または文字列 (4) を比較できます。 Account より小さい演算子はサポートされていません。それにもかかわらず、私は Account を比較できます 秒。 (3)。さらに、辞書順ではなく内部番号 (4) に基づいて文字列を比較したいと考えています。 (3) と (4) の 2 つのラムダ式を 2 項述語として提供することで、仕事をうまくこなすことができました。

次は?

この投稿の冒頭にある図をよく見てみると、テンプレートの基本について説明したことがお分かりいただけると思います。テンプレートに関する次回の投稿では、さらに詳細を掘り下げ、テンプレートのインスタンス化について書きます。