#45 すべての互換型を受け取るために,メンバ関数テンプレートを使う
Effective C++ 45項の内容です.
テンプレートで互換型を受け取るには?
「スマートポインタ(=リソース管理オブジェクト)を使えばリソースの破棄等を適切な範囲でやってくれるから,生のポインタを使わずにスマートポインタを使おう!」というのが #13 の内容でした.ここで,スマートポインタというからには,生のポインタでできることはすべてスマートポインタでもできなければいけません.ここでは,基底クラスと派生クラスのポインタの暗黙の型変換を引き合いにだして,これをどのようにテンプレートを使って実現するかの説明がされてます.手っ取り早く,サンプルコードですが,継承関係がある場合,基底クラスのポインタに継承クラスのポインタを入れることはOKでした.下記はサンプルコードですが,ChildクラスのポインタをParentクラスのポインタとして保持することができます.(で,関数を virtual 宣言してあげれば,実際のクラスのメソッドが呼ばれるのでした.)
#include <iostream> class Parent { public: virtual ~Parent() {} virtual void say() { std::cout << "Parent" << std::endl; } }; class Child : public Parent { public: virtual void say() {std::cout << "Child" << std::endl; } }; int main(int, char**) { std::cout << "chap_7th_44_1.cpp" << std::endl; // ポインタの暗黙の型変換. Child *c = new Child; Parent *p = c; p->say(); return 0; }
で,生のポインタのこの挙動「ポインタの暗黙の型変換」をスマートポインタで実装するにはどうしたらいいでしょうか?何も考えずに下記のように実装すると,コンパイルエラーになります.
#include <iostream> class Parent { public: virtual ~Parent() {} virtual void say() { std::cout << "Parent" << std::endl; } }; class Child : public Parent { public: virtual void say() {std::cout << "Child" << std::endl; } }; template <typename T> class SmartPtr { public: explicit SmartPtr(T *realPtr) : ptr(realPtr) {} private: T *ptr; }; int main(int, char**) { std::cout << "chap_7th_44_2.cpp" << std::endl; // ポインタでできるなら,スマートポインタでも同じことができるはず! SmartPtr<Child> sc = SmartPtr<Child>(new Child); SmartPtr<Parent> sp = sc; return 0; }
この時に出るコンパイルエラーは下記ですが,これは確かにそのとおりで,タイプパラメータ Parent と Child の間には継承関係がありますが,SmartPtr<Parent>とSmartPtr<Child>の間にはなんの関係もありません.完全に別クラスです.
error: conversion from ‘SmartPtr<Child>’ to non-scalar type ‘SmartPtr<Parent>’ requested SmartPtr<Parent> sp = sc;
ということで,生のポインタらしく振る舞わせるためには ”SmartPtr<Parent> sp = sc” の一行で何かがおこらないといけません.何かを起こすには,,,,,そうです,このケースだとコピーコンストラクタを定義してあげればいいのです!ということで,テンプレート特化を使って下記のような実装を作れば,コンパイルが通る様になります.
#include <iostream> class Parent { public: virtual ~Parent() {} virtual void say() { std::cout << "Parent" << std::endl; } }; class Child : public Parent { public: virtual void say() {std::cout << "Child" << std::endl; } }; template <typename T> class SmartPtr { public: explicit SmartPtr(T *realPtr) : ptr(realPtr) {} T* getPtr() const { return this->ptr; } private: T *ptr; }; template<> class SmartPtr<Parent> { public: SmartPtr(const SmartPtr<Child>& other) : ptr(other.getPtr()) {} private: Parent *ptr; }; int main(int, char**) { std::cout << "chap_7th_44_2.cpp" << std::endl; // ポインタでできるなら,スマートポインタでも同じことができるはず! SmartPtr<Child> sc = SmartPtr<Child>(new Child); // 暗黙的にコピーコンストラクタが呼ばれる. SmartPtr<Parent> sp = sc; return 0; }
で,問題はここからです.上記の例ではテンプレート特化を使って SmartPtr<Child> -> SmartPtr<Parent> を成立させましたが,すべてのケースにおいてテンプレート特化を書くことは不可能です.で,テンプレートパラメータとして指定されたTのクラスの中で,Uの(別のテンプレートパラメータ)が指定された時の振る舞いを記述する方法がこの項では述べられています.具体的には,下記のように実装すればテンプレート特化なしで上記の目的が達成されます.
#include <iostream> class Parent { public: virtual ~Parent() {} virtual void say() { std::cout << "Parent" << std::endl; } }; class Child : public Parent { public: virtual void say() {std::cout << "Child" << std::endl; } }; template <typename T> class SmartPtr { public: explicit SmartPtr(T *realPtr) : ptr(realPtr) {} // Tというテンプレートパラメータが指定されているクラスで, // 別のテンプレートパラメータが指定された時の振る舞いを記述. template<typename U> SmartPtr(const SmartPtr<U> &other) : ptr(other.getPtr()) {} T* getPtr() const { return this->ptr; } private: T *ptr; }; int main(int, char**) { std::cout << "chap_7th_44_4.cpp" << std::endl; // ポインタでできるなら,スマートポインタでも同じことができるはず! SmartPtr<Child> sc = SmartPtr<Child>(new Child); // 暗黙的にコピーコンストラクタが呼ばれる. SmartPtr<Parent> sp = sc; return 0; }
上記の様に書けば,SmartPtrが生のポインタのように振るまいます.ここで,もしTとUの間に継承関係がなければ,SmartPtrのコピーコンストラクタの時点でエラーになります.上記の例では,コピーコンストラクタを扱いました.では,次の代入のケースはどうでしょうか.
#include <iostream> class Parent { public: virtual ~Parent() {} virtual void say() { std::cout << "Parent" << std::endl; } }; class Child : public Parent { public: virtual void say() {std::cout << "Child" << std::endl; } }; template <typename T> class SmartPtr { public: SmartPtr() {} explicit SmartPtr(T *realPtr) : ptr(realPtr) {} // Tというテンプレートパラメータが指定されているクラスで, // 別のテンプレートパラメータが指定された時の振る舞いを記述. template<typename U> SmartPtr(const SmartPtr<U> &other) : ptr(other.getPtr()) { std::cout << "SmartPtr(const SmartPtr<U> &other)" << std::endl; } template<typename U> SmartPtr& operator=(SmartPtr<U> &rhs) { std::cout << "SmartPtr& operator=(SmartPtr<U> &rhs)" << std::endl; this->ptr = rhs.getPtr(); return *this; } T* getPtr() const { return this->ptr; } private: T *ptr; }; int main(int, char**) { std::cout << "chap_7th_44_5.cpp" << std::endl; // ポインタでできるなら,スマートポインタでも同じことができるはず! SmartPtr<Child> sc = SmartPtr<Child>(new Child); // 暗黙的にコピーコンストラクタが呼ばれる. SmartPtr<Parent> sp = SmartPtr<Parent>(new Parent); sp = sc; std::cout << sp.getPtr() << std::endl; std::cout << sc.getPtr() << std::endl; return 0; }
上記のコードを実行すると,定義された代入演算子 "SmartPtr<Parent>& operator=(SmartPtr<Child> &rhs)" が呼ばれることがわかります.Effective C++では,このようなコピーコンストラクタ,代入演算子を一般化されたコピーコンストラクタ,一般化された代入演算子とよんでいました.
一般化されたコピーコンストラクタ,一般化された代入演算子を定義して,コンパイラは通常のコピーコンストラクタ,代入演算子を生成してしまう
ということで,ここでは下記のサンプルコードの挙動を見てみます.
#include <iostream> class Parent { public: virtual ~Parent() {} virtual void say() { std::cout << "Parent" << std::endl; } }; class Child : public Parent { public: virtual void say() {std::cout << "Child" << std::endl; } }; template <typename T> class SmartPtr { public: explicit SmartPtr(T *realPtr) : ptr(realPtr) { std::cout << "explicit SmartPtr(T *realPtr)" << std::endl; } // Tというテンプレートパラメータが指定されているクラスで, // 別のテンプレートパラメータが指定された時の振る舞いを記述. template<typename U> SmartPtr(const SmartPtr<U> &other) : ptr(other.getPtr()) { std::cout << "SmartPtr(const SmartPtr<U> &other)" << std::endl; } T* getPtr() const { return this->ptr; } private: T *ptr; }; int main(int, char**) { std::cout << "chap_7th_44_6.cpp" << std::endl; std::cout << "Explicit Copy Constructor Call" << std::endl; SmartPtr<Child> sc = SmartPtr<Child>(new Child); // 暗黙的にコピーコンストラクタが呼ばれる. std::cout << "Implicit Copy Constructor Call" << std::endl; SmartPtr<Child> sc2 = sc; // 暗黙的にコピーコンストラクタが呼ばれる. std::cout << "Implicit Copy Constructor Call" << std::endl; SmartPtr<Parent> sp = sc; return 0; }
上記サンプルコード,main関数の "SmartPtr<Child> sc2 = sc;" と "SmartPtr<Parent> sp = sc;" では,同じように暗黙的にコピーコンストラクタが呼ばれますが,
"SmartPtr<Child> sc2 = sc;"
の実行では,文字列 "std::cout << "SmartPtr(const SmartPtr<U> &other)" << std::endl;" が出力されず,
"SmartPtr<Parent> sp = sc;"
の実行では,文字列 "std::cout << "SmartPtr(const SmartPtr<U> &other)" << std::endl;" が出力されます.この理由は,SmartPtr<Child> = sc では,コンパイラが自動生成したデフォルトのコピーコンストラクタが呼ばれているからです.つまり,一般化されたコピーコンストラクタ,コピー代入演算子を宣言・定義しても,コンパイラはそれらの自動生成を抑制しません.そのため,コンパイラがそれらを自動生成することを抑制したければ,”一般化していない”コピーコンストラクタ・コピー代入演算子を宣言・定義する必要があります.