C++ コア ガイドライン:関数テンプレートの特殊化に含まれるサプライズ

今日、多くの C++ 開発者にとって大きな驚きであるテンプレートに対する C++ コア ガイドライン ルールを完成させます。関数テンプレートの特殊化について書いています。

簡単に始めましょう。これは鳥瞰的な観点からのテンプレートの特殊化です。

テンプレートの専門化

テンプレートは、クラスと関数のファミリの動作を定義します。多くの場合、特殊なタイプまたは非タイプを特別に扱う必要があります。このユース ケースをサポートするには、テンプレートを完全に専門化します。クラス テンプレートは、部分的に特殊化することもできます。

一般的なアイデアを得るためのコード スニペットを次に示します。

template <typename T, int Line, int Column> // (1)
class Matrix;

template <typename T> // (2)
class Matrix<T, 3, 3>{};

template <> // (3)
class Matrix<int, 3, 3>{};

行 1 は、プライマリまたは一般的なテンプレートです。このテンプレートは少なくとも宣言する必要があり、部分的または完全に特殊化されたテンプレートの前に宣言する必要があります。 2 行目は、部分的な特殊化が続きます。行 3 は完全な特殊化です。

部分的および完全な専門化をよりよく理解するために、視覚的な説明を提示したいと思います。テンプレート パラメーターの n 次元空間について考えてみましょう。プライマリ テンプレート (1 行目) では、任意の型と 2 つの任意の int を選択できます。 2行目の部分特化の場合は型しか選べません。これは、3 次元空間が線に縮小されることを意味します。完全な特殊化とは、3 次元空間に 1 つのポイントがあることを意味します。

テンプレートを呼び出すとどうなりますか?

Matrix<int, 3, 3> m1; // class Matrix<int, 3, 3>

Matrix<double, 3, 3> m2; // class Matrix<T, 3, 3> 

Matrix<std::string, 4, 3> m3; // class Matrix<T, Line, Column> => ERROR

m1 は完全な特殊化を使用し、m2 は部分的な特殊化を使用し、m3 はプライマリ テンプレートを使用しますが、定義がないためにエラーが発生します。

適切な特殊化を行うためにコンパイラが使用する 3 つの規則を次に示します。

<オール>
  • コンパイラは特殊化を 1 つだけ見つけます。コンパイラは特殊化を使用します。
  • コンパイラが複数の特殊化を検出しました。コンパイラは、最も特殊化されたものを使用します。このプロセスが複数の特殊化で終了した場合、コンパイラはエラーをスローします。
  • コンパイラは特殊化を検出しません。主要な専門分野を使用します。
  • さて、A が B よりも特化したテンプレートであることを説明する必要があります。 cppreference.com の非公式な定義は次のとおりです。「A は、B が受け入れるタイプのサブセットを受け入れます ".

    最初の概要の後、関数テンプレートをもう少し深く掘り下げることができます

    関数テンプレートの特殊化とオーバーロード

    関数テンプレートを使用すると、テンプレートの特殊化の作業が簡単になりますが、同時に難しくなります。

    • 関数テンプレートは完全な特殊化のみをサポートするため、簡単です。
    • 関数のオーバーロードが発生するため、さらに難しくなります。

    設計の観点から、テンプレートの特殊化またはオーバーロードを使用して関数テンプレートを特殊化できます。

    // functionTemplateSpecialisation.cpp
    
    #include <iostream>
    #include <string>
    
    template <typename T> // (1)
    std::string getTypeName(T){
     return "unknown type";
    }
    
    template <> // (2)
    std::string getTypeName<int>(int){
     return "int";
    }
    
    std::string getTypeName(double){ // (3)
     return "double";
    }
    
    int main(){
     
     std::cout << std::endl;
     
     std::cout << "getTypeName(true): " << getTypeName(true) << std::endl;
     std::cout << "getTypeName(4711): " << getTypeName(4711) << std::endl;
     std::cout << "getTypeName(3.14): " << getTypeName(3.14) << std::endl;
     
     std::cout << std::endl;
     
    }
    

    行 1 にはプライマリ テンプレートがあり、行 2 には int の完全な特殊化があり、行 3 には double のオーバーロードがあります。関数または関数テンプレートの値には興味がないので、スキップしました。たとえば、std::string getTypeName(double) です。さまざまな機能の使用は非常に快適です。コンパイラは型を推測し、正しい関数または関数テンプレートが呼び出されます。関数のオーバーロードの場合、関数のオーバーロードが完全に適合する場合、コンパイラは関数テンプレートよりも関数のオーバーロードを優先します。

    しかし、投稿のタイトルで述べた大きな驚きはどこにあるのでしょうか?ここにあります。

    T.144:関数テンプレートを特化しない

    ルールの理由は非常に短く、関数テンプレートの特殊化はオーバーロードに参加しません。それが何を意味するか見てみましょう。私のプログラムは、Demiov/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 << std::endl;
     
     int *p;
     
     std::cout << "getTypeName(p): " << getTypeName(p) << std::endl; 
     std::cout << "getTypeName2(p): " << getTypeName2(p) << std::endl; 
     
     std::cout << std::endl;
     
    }
    

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

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

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

    次は?

    この行を校正しているときに、私は考えを思いつきました。テンプレートは、より多くのサプライズに適しています。そのため、コア ガイドラインから少し離れて、いくつかを紹介します。このセリフに出くわしたら、覚えておいていただければ幸いです。

    C++ の未来はテンプレートを語ります。したがって、彼らの言語についてもっと知ることは良いことです.