テンプレート:誤解と驚き

私はよくテンプレートの基本を教えます。テンプレートは特別です。したがって、私は驚きを引き起こす多くの誤解に遭遇します。それらのいくつかを次に示します。

私の最初の誤解は、おそらく多くの C++ 開発者にとって明らかですが、すべての C++ 開発者にとってそうではありません。

そもそも関連型って何?これは、暗黙的に変換できる型を表す非公式の用語です。ここが出発点です。

// genericAssignment.cpp

#include <vector>

template <typename T, int N> // (1)
struct Point{
 Point(std::initializer_list<T> initList): coord(initList){}

 std::vector<T> coord; 
};

int main(){

 Point<int, 3> point1{1, 2, 3};
 Point<int, 3> point2{4, 5, 6};
 
 point1 = point2; // (2)
 
 auto doubleValue = 2.2; 
 auto intValue = 2;
 doubleValue = intValue; // (3)
 
 Point<double, 3> point3{1.1, 2.2, 3.3};
 point3 = point2; // (4)

}

クラス テンプレート Point は、n 次元空間内の点を表します。座標のタイプと寸法を調整できます (1 行目)。座標は std::vector に格納されます。座標タイプと次元が同じ 2 つの点を作成すると、それらを割り当てることができます。

今、誤解が始まります。 int を double に割り当てることができます (3 行目)。したがって、int の Point を double の Point に割り当てることができるはずです。 C++ コンパイラは、行 4 について非常に具体的です。両方のクラス テンプレートは関連付けられておらず、割り当てることができません。それらは異なるタイプです。

エラー メッセージは、最初のヒントを示します。 Point から Point への変換をサポートする代入演算子が必要です。クラス テンプレートに汎用コピー代入演算子が追加されました。

// genericAssignment2.cpp

#include <algorithm>
#include <iostream>
#include <string>
#include <vector>

template <typename T, int N>
struct Point{

 Point(std::initializer_list<T> initList): coord(initList){}

 template <typename T2> 
 Point<T, N>& operator=(const Point<T2, N>& point){ // (1)
 static_assert(std::is_convertible<T2, T>::value, 
 "Cannot convert source type to destination type!");
 coord.clear();
 coord.insert(coord.begin(), point.coord.begin(), point.coord.end());
 return *this;
 }
 
 std::vector<T> coord;
 
};


int main(){

 Point<double, 3> point1{1.1, 2.2, 3.3};
 Point<int, 3> point2{1, 2, 3};
 
 Point<int, 2> point3{1, 2};
 Point<std::string, 3> point4{"Only", "a", "test"};

 point1 = point2; // (3)
 
 // point2 = point3; // (4)
 // point2 = point4; // (5)

}

(1) 行により、(3) 行のコピー割り当てが機能します。クラステンプレートを詳しく見てみましょう ポイント:

  • Point&operator=(const Point&point):割り当てられた Point は Point 型であり、同じ次元を持つ Point のみを受け入れますが、タイプは異なる場合があります:Point.
  • static_assert(std::is_convertible::value, "Cannot convert source type to destination type!"):この式は、型特性ライブラリの関数 std::is_convertible を使用してチェックします。 T2 を T に変換できる場合

行 (4) と (5) を使用すると、コンパイルが失敗します:

行 (3) は、両方の点の次元が異なるため、エラーになります。行 (4) は代入演算子で static_assert をトリガーします。これは、std::string が int に変換できないためです。

次の誤解はもっと驚くべき可能性を秘めていると思います.

クラス テンプレートから継承されたメソッド自体は利用できません

簡単に始めましょう。

// inheritance.cpp

#include <iostream>

class Base{
public:
 void func(){ // (1)
 std::cout << "func" << std::endl;
 }
};

class Derived: public Base{
public:
 void callBase(){
 func(); // (2)
 }
};

int main(){

 std::cout << std::endl;

 Derived derived;
 derived.callBase(); 

 std::cout << std::endl;

}

クラス Base と Derived を実装しました。 Derived は Base から派生した public であるため、そのメソッド callBase (2 行目) クラス Base のメソッド func で使用できます。わかりました、プログラムの出力に追加するものは何もありません.

Base をクラス テンプレートにすることで、動作が完全に変わります。

// templateInheritance.cpp

#include <iostream>

template <typename T>
class Base{
public:
 void func(){ // (1)
 std::cout << "func" << std::endl;
 }
};

template <typename T>
class Derived: public Base<T>{
public:
 void callBase(){
 func(); // (2)
 }
};

int main(){

 std::cout << std::endl;

 Derived<int> derived;
 derived.callBase(); 

 std::cout << std::endl;

}

コンパイル エラーに驚かれるかもしれません。

エラーメッセージの「テンプレートパラメーターに依存する 'func' への引数はないため、'func' の宣言を使用できる必要があります」という行が最初のヒントです。 func は、その名前がテンプレート パラメーター T に依存しないため、いわゆる非依存の名前です。その結果、コンパイラは from T 依存基底クラス Base を参照せず、外部で使用できる名前 func はありません。クラス テンプレート。

名前ルックアップを依存基底クラスに拡張するには、3 つの回避策があります。次の例では、3 つすべてを使用しています。

// templateInheritance2.cpp

#include <iostream>

template <typename T>
class Base{
public:
 void func1() const {
 std::cout << "func1()" << std::endl;
 }
 void func2() const {
 std::cout << "func2()" << std::endl;
 }
 void func3() const {
 std::cout << "func3()" << std::endl;
 }
};

template <typename T>
class Derived: public Base<T>{
public:
 using Base<T>::func2; // (2)
 void callAllBaseFunctions(){

 this->func1(); // (1)
 func2(); // (2)
 Base<T>::func3(); // (3)

 }
};


int main(){

 std::cout << std::endl;

 Derived<int> derived;
 derived.callAllBaseFunctions();

 std::cout << std::endl;

}

  • 名前を依存させる :1 行目の this->func1 の呼び出しは、これが暗黙的に依存しているため、依存しています。この場合、名前検索ではすべての基本クラスが考慮されます。
  • 名前を現在のスコープに導入: Base::func2 を使用する式 (2 行目) は、func2 を現在のスコープに導入します。
  • 名前を完全修飾して呼ぶ :func3 を完全修飾 (3 行目) で呼び出すと、仮想ディスパッチが中断され、新たな驚きが生じる可能性があります。

最後に、これがプログラムの出力です。

次は?

従属名については、次の投稿でもっと書きたいことがあります。場合によっては、依存する名前を typename または template で明確にする必要があります。これを初めて見た場合、おそらく私と同じくらい驚くでしょう。