C++ コア ガイドライン:テンプレートによる型消去

前回の記事「C++ コア ガイドライン:型消去」では、型消去を実装する 2 つの方法、つまり void ポインターとオブジェクト指向を紹介しました。この投稿では、動的ポリモーフィズム (オブジェクト指向) と静的ポリモーフィズム (テンプレート) を橋渡しして、テンプレートで型消去を取得します。

出発点として、また念のため、オブジェクト指向に基づいた型消去を次に示します。

オブジェクト指向による型消去

オブジェクト指向による型消去は、継承階層に要約されます。

// typeErasureOO.cpp

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

struct BaseClass{ // (2)
 virtual std::string getName() const = 0;
};

struct Bar: BaseClass{
 std::string getName() const override {
 return "Bar";
 }
};

struct Foo: BaseClass{
 std::string getName() const override{
 return "Foo";
 }
};

void printName(std::vector<const BaseClass*> vec){ // (3)
 for (auto v: vec) std::cout << v->getName() << std::endl;
}


int main(){
 
 std::cout << std::endl;
 
 Foo foo;
 Bar bar; 
 
 std::vector<const BaseClass*> vec{&foo, &bar}; // (1)
 
 printName(vec);
 
 std::cout << std::endl;

}

キーポイントは、 Foo のインスタンスを使用できることです または Bar BaseClass. のインスタンスの代わりに 詳細については、投稿 C++ コア ガイドライン:型消去をお読みください。

OO を使用したこの実装の長所と短所は何ですか?

長所:

  • タイプセーフ
  • 実装が簡単

短所:

  • バーチャル ディスパッチ
  • 派生クラスはそのベースについて知る必要があるため、煩わしい

テンプレートを使用した消去型のどの欠点が解決されるか見てみましょう。

テンプレートによるタイプ消去

これは、以前の OO プログラムに対応するテンプレート プログラムです。

// typeErasure.cpp

#include <iostream>
#include <memory>
#include <string>
#include <vector>

class Object { // (2)
 
public:
 template <typename T> // (3)
 Object(T&& obj): object(std::make_shared<Model<T>>(std::forward<T>(obj))){}
 
 std::string getName() const { // (4)
 return object->getName(); 
 }
 
 struct Concept { // (5)
 virtual ~Concept() {}
 virtual std::string getName() const = 0;
 };

 template< typename T > // (6)
 struct Model : Concept {
 Model(const T& t) : object(t) {}
 std::string getName() const override {
 return object.getName();
 }
 private:
 T object;
 };

 std::shared_ptr<const Concept> object;
};


void printName(std::vector<Object> vec){ // (7)
 for (auto v: vec) std::cout << v.getName() << std::endl;
}

struct Bar{
 std::string getName() const { // (8)
 return "Bar";
 }
};

struct Foo{
 std::string getName() const { // (8)
 return "Foo";
 }
};

int main(){
 
 std::cout << std::endl;
 
 std::vector<Object> vec{Object(Foo()), Object(Bar())}; // (1)
 
 printName(vec);
 
 std::cout << std::endl;

}

さて、ここで何が起こっているのですか? Object という名前にイライラしないでください 、 Concept 、および Model .それらは通常、文献の型消去に使用されます。だから私はそれらに固執します.

初めに。私の std: :vector は Object 型のインスタンス (1) を使用します (2) 最初の OO の例のようなポインターではありません。このインスタンスは、ジェネリック コンストラクター (3) を持っているため、任意の型で作成できます。オブジェクトには getName があります getName に直接転送されるメソッド (4) オブジェクトの。オブジェクトのタイプは std::shared_ptr<const Concept> です . getName Concept の方法 純粋な仮想 (5) であるため、仮想ディスパッチにより getName Model の方法 (6) を使用します。結局、getName Bar のメソッド と Foo (8) は printName で適用されます 関数 (7)。

これがプログラムの出力です。

もちろん、この実装は型安全です。

エラー メッセージ

私は現在C++クラスを提供しています。テンプレートのエラー メッセージについてよく話し合います。したがって、クラス Foo を変更した場合のエラー メッセージに興味がありました。 と Bar 若干。間違った実装は次のとおりです:

struct Bar{
 std::string get() const { // (1)
 return "Bar";
 }
};

struct Foo{
 std::string get_name() const { // (2)
 return "Foo";
 }
};

メソッドの名前を getName に変更しました get へ (1) そして get_name (2). 

以下は、Compiler Explorer からコピーされたエラー メッセージです。

Clang 6.0.0 の最も醜いものから始めて、GCC 8.2 の非常に優れたもので終わります。 MSVC 19 からのエラー メッセージは、その中間です。正直なところ、clang が最も明確なエラー メッセージを生成すると思っていたので、私はまったく驚きました。

Clang 6.0.0

1 つのスクリーンショットでは大きすぎるため、エラー メッセージの半分しか表示できません。

MSVC 19

GCC 8.2

GCC 8.2 のスクリーンショットをよく見てください。 「 27:20:エラー:'const struct Foo' には 'getName' という名前のメンバーがありません。'get_name' のことですか?」と表示されます。 いいですね!

MSVC からのエラー メッセージ、特に Clang からのエラー メッセージはかなりひどいものです。これで私の投稿は終わりではありません。

私の挑戦

ここで、課題を解決したいと思います。特定のクラスに特定のメソッドがあるかどうかをコンパイル時に検出するにはどうすればよいですか。この場合、クラス BarFoo メソッド getName. が必要です 私は SFINAE で遊んで、C++11 バリアント std::enable_if で実験し、ライブラリの基本的な TS v2 の一部である検出イディオムで終了しました。これを使用するには、実験的な名前空間 (1) のヘッダーを含める必要があります。変更された例は次のとおりです:

// typeErasureDetection.cpp

#include <experimental/type_traits> // (1) 

#include <iostream>
#include <memory>
#include <string>
#include <vector>

template<typename T>
using getName_t = decltype( std::declval<T&>().getName() ); // (2)

class Object { 
 
public:
 template <typename T> 
 Object(T&& obj): object(std::make_shared<Model<T>>(std::forward<T>(obj))){ // (3)
 
 static_assert(std::experimental::is_detected<getName_t, decltype(obj)>::value, 
 "No method getName available!");
 
 }
 
 std::string getName() const { 
 return object->getName(); 
 }
 
 struct Concept { 
 virtual ~Concept() {}
 virtual std::string getName() const = 0;
 };

 template< typename T > 
 struct Model : Concept {
 Model(const T& t) : object(t) {}
 std::string getName() const override {
 return object.getName();
 }
 private:
 T object;
 };

 std::shared_ptr<const Concept> object;
};


void printName(std::vector<Object> vec){ 
 for (auto v: vec) std::cout << v.getName() << std::endl;
}

struct Bar{
 std::string get() const { 
 return "Bar";
 }
};

struct Foo{
 std::string get_name() const { 
 return "Foo";
 }
};

int main(){
 
 std::cout << std::endl;
 
 std::vector<Object> vec{Object(Foo()), Object(Bar())}; 
 
 printName(vec);
 
 std::cout << std::endl;

}

(1)、(2)、(3) の行を追加しました。行 (2) はメンバー関数の型を推測します getName() . C++11 の std::declval は、decltype でメンバー関数を使用できるようにする関数です。 オブジェクトを構築する必要のない式。検出イディオムの重要な部分は関数 std::experimental::is_detectedです static_assert の型特性ライブラリから (3).

Compiler Explorer でプログラムを実行すると、Clang 6.0.0 が生成するものを見てみましょう:

わお!それはまだ出力が多すぎます。実を言うと。この機能の状態はまだ実験段階です。エラー メッセージの出力を注意深く見て、static_assert, を検索すると、 あなたが探している答えが見つかります。出力の最初の 3 行を次に示します。

すごい!少なくとも、文字列 "No method getName available を grep できます。 " エラー メッセージに表示されます。

投稿を終了する前に、テンプレートを使用した型消去の長所と短所を次に示します。

長所:

  • タイプセーフ
  • 派生クラスは基本クラスを知る必要がないため、邪魔にならない

短所:

  • バーチャル ディスパッチ
  • 実装が難しい

最後に、オブジェクト指向とテンプレートの型消去の違いは、主に次の 2 つの点に要約されます。

  • 邪魔にならないものと邪魔にならないもの
  • 実装が容易か困難か

次は?

これで私の回り道は終わりです。次の投稿では、ジェネリック プログラミングの旅を続けます。より具体的には、コンセプトについて書きます。