#3(の1)可能ならいつでも const を使う.
Effective C++ 3項の内容です.
const の使い方一覧
constがとても大事なことは,Effective C++も言っているし,とても良くわかるんですが,constの位置によってconstの対象が変わるのがなかなか覚えられません...一通りリストアップします.
"const" for 変数編
変数に対しての const 宣言としては,主に下記の5つがあります.Effective C++では,下記のような覚え方が提唱されていました.
「アスタリスク(*)よりも const が右側に来ていたら,データが const.左側に来ていたら,ポインタが const ということだそうです.」
- 1.データがconst
const int a = 0;
- 2.ポインタがconst,ポインタの指すデータは非const
int * const a;
- 3.ポインタが非const,ポインタの指すデータはconst
int const * a
- 4.ポインタもconst,ポインタの指すデータもconst
int const * const d = 10
ということで,サンプルコードです.
#include <iostream> int main(int, char**) { std::cout << "Effective C++ 3項" << std::endl; // 1. データが const // 変数に対するconst宣言 const double a = 0; // error: assignment of read-only variable ‘a’ // a = 1; // 2. ポインタが非 const, データが const const int *b = 0; // データの更新は許されない. // error : assignment of read-only location ‘* b’ //*b = 10; // ポインタの更新は許される. // This is possible. b = nullptr; // 3. ポインタが const,データが 非const int * const c = nullptr; int d = 0; // データの更新は許される. *c = 100; // ポインタの更新は許されない. // error : assignment of read-only variable ‘c’ // c = &d; // 4. ポインタ,データともに const int const * const e = 0; // error : assignment of read-only location ‘*(const int*)e’ //*e = 10; // error: assignment of read-only variable ‘e’ // e = nullptr; return 0; }
#24(の2) すべての引数に型変換が必要なら,非メンバ関数を定義する.
ということで前回エントリの続きです.
演算子の定義
前回整数値からRationalへの暗黙の型変換をするには,コンストラクタが使われていることを実験しました.今回はもう一歩進んで,掛け算の演算子オペレータを定義したいと思います.早速サンプルコードですが,
#include <iostream> #include <string> class Rational { public: Rational(int numerator=0, int denominator=1) : m_numerator(numerator), m_denominator(denominator) { std::cout << "Rational(int numerator, int denominator)" << std::endl; } ~Rational() { std::cout << "~Rational()" << std::endl; } Rational(const Rational &rhs) { this->m_numerator = rhs.m_numerator; this->m_denominator = rhs.m_denominator; std::cout << "Rational(const Rational &rhs)" << std::endl; } Rational& operator=(const Rational& rhs) { std::cout << "operator=(const Rational& rhs)" << std::endl; m_numerator = rhs.m_numerator; m_denominator = rhs.m_denominator; return *this; } Rational operator*(const Rational& rhs) { std::cout << "operator*(const Rational& rhs)" << std::endl; return Rational(this->m_numerator * rhs.m_numerator, this->m_denominator * rhs.m_denominator); } void say() { std::cout << "m_numerator : " << m_numerator << std::endl; std::cout << "m_denominator : " << m_denominator << std::endl; } int m_numerator; int m_denominator; }; int main(int, char**) { std::cout << "chap_4th_24_3.cpp" << std::endl; // 100からの暗黙の型変換. Rational r = 100; // 2からの暗黙の型変換 + 掛け算の実行. Rational ans = r * 2; Rational ans2 = r.operator*(Rational(2)); ans.say(); ans2.say(); return 0; }
上記の実行結果は下記のようになります.
chap_4th_24_3.cpp Rational(int numerator, int denominator) Rational(int numerator, int denominator) operator*(const Rational& rhs) Rational(int numerator, int denominator) ~Rational() Rational(int numerator, int denominator) operator*(const Rational& rhs) Rational(int numerator, int denominator) ~Rational() m_numerator : 200 m_denominator : 1 m_numerator : 200 m_denominator : 1 ~Rational() ~Rational() ~Rational()
ということで,掛け算が演算子 "*" で無事に実行されることがわかりましたが,関数呼び出しを追いかけてみると掛け算の一行で複数の関数が呼ばれていることがわかります.
1.r * 2 の ”2” の Rational への暗黙の型変換のためのコンストラクタ呼び出し.
2.r * 2 の ”*” のための掛け算オペレータ呼び出し.
また,"r * 2" の演算は実際には下記の関数呼び出しと等価であることがわかります.(し,実際にこう書いてもコンパイル&実行できます.)
r.operator*(Rational(2))
上記の様に書けば結局は自分がクラスの中に定義した演算子オペレータとコンストラクタが呼び出されていることがはっきりと認識できます.で,Effective C++はもう一歩進んで,掛け算の順序入れ替えの話を持ち出してきます.数学的には,下記の演算は同じになるはずです.
"r * 2 と 2 * r"
では,下記のコードも実行できるかというと....
int main(int, char**) { std::cout << "chap_4th_24_3.cpp" << std::endl; // 100からの暗黙の型変換. Rational r = 100; // 順番入れ替え Rational ans3 = 2 * r; Rational ans4 = 2.operator*(r); ans.say(); ans2.say(); return 0; }
当然のことながらコンパイルエラーになります.これも,明示的にすべてを書き出してみると明らかで,"2.operator*(r)" なんて関数がどこにも定義されていないことことが原因です.というわけで,Rationalのオブジェクト2つを引数に取るメソッド(演算子オペレータ)の定義が必要になります.
#include <iostream> #include <string> class Rational { public: Rational(int numerator=0, int denominator=1) : m_numerator(numerator), m_denominator(denominator) { std::cout << "Rational(int numerator, int denominator)" << std::endl; } ~Rational() { std::cout << "~Rational()" << std::endl; } Rational(const Rational &rhs) { this->m_numerator = rhs.m_numerator; this->m_denominator = rhs.m_denominator; std::cout << "Rational(const Rational &rhs)" << std::endl; } Rational& operator=(const Rational& rhs) { std::cout << "operator=(const Rational& rhs)" << std::endl; m_numerator = rhs.m_numerator; m_denominator = rhs.m_denominator; return *this; } void say() { std::cout << "m_numerator : " << m_numerator << std::endl; std::cout << "m_denominator : " << m_denominator << std::endl; } int m_numerator; int m_denominator; }; const Rational operator*(const Rational &lhs, const Rational &rhs) { std::cout << "operator*(const Rational &lhs, const Rational &rhs)" << std::endl; return Rational(lhs.m_numerator * rhs.m_numerator, rhs.m_denominator * rhs.m_denominator); } int main(int, char**) { std::cout << "chap_4th_24_3.cpp" << std::endl; // 100からの暗黙の型変換. Rational r = 100; Rational ans1 = r * 2; Rational ans2 = operator*(r, 2); // 順番入れ替え Rational ans3 = 2 * r; Rational ans4 = operator*(2, r); ans3.say(); ans4.say(); return 0; }
上記コードの実装では,掛け算の順番入れ替えにも対応できました.一点注目してほしい箇所としては,前までのサンプルコードにあった,Rationalクラス中の演算子オペレータを削除してます.なぜかというと,クラス外に定義した非メンバ関数で両方対応できるからです.最終的に,これが24項の主張している"thisを含めたすべての引数に型変換が必要なら,メンバでない関数を宣言しよう"となります.
#24(の1) すべての引数に型変換が必要なら,非メンバ関数を定義する.
ということで,Effective C++24項の内容です.
コンストラクタと暗黙の型変換
Effective CPPでは,有理数の例を用いて暗黙の型変換が好まれる例を説明していました.他に簡単&適切な例が思いつかないので,ここでも有理数の例を用いて実験したいと思います.まず,下記のコードを書いた時になにが起こっているかを見てみます.
#include <iostream> #include <string> class Rational { public: Rational(int numerator=0, int denominator=1) : m_numerator(numerator), m_denominator(denominator) { std::cout << "Rational(int numerator, int denominator)" << std::endl; } ~Rational() { std::cout << "~Rational()" << std::endl; } Rational(const Rational &rhs) { this->m_numerator = rhs.m_numerator; this->m_denominator = rhs.m_denominator; std::cout << "Rational(const Rational &rhs)" << std::endl; } Rational& operator=(const Rational& rhs) { std::cout << "operator=(const Rational& rhs)" << std::endl; m_numerator = rhs.m_numerator; m_denominator = rhs.m_denominator; return *this; } int m_numerator; int m_denominator; }; int main(int, char**) { std::cout << "chap_4th_24_1.cpp" << std::endl; Rational r = 100; std::cout << "r.m_numerator : " << r.m_numerator << std::endl; std::cout << "r.m_denominator : " << r.m_denominator << std::endl; return 0; }
上記サンプルコードの実行結果は下記のようになります.
chap_4th_24_1.cpp Rational(int numerator, int denominator) r.m_numerator : 100 r.m_denominator : 1 ~Rational()
明示的にコンストラクタを呼んでいませんが,”Rational r = 100” の部分でコンストラクタが暗黙的に呼ばれています.で,実際問題どのようにこれが起こっているかというと...
1.Rationalクラスのオブジェクト r が整数100で初期化されようとしている.
2.しかし,Rationalクラスと整数100の型が異なるため,コピーコンストラクタは呼び出せず,整数100からRationalを生成する方法をコンパイラが探す.
3.Rationalクラスのコンストラクタでは,デフォルト引数が指定されているため,numeratorにだけ100を指定すればRationalクラスをRational(100, 1)として生成する.(<-暗黙の型変換)
で,コンストラクタにexplicitと書けばこの暗黙の型変換がされなくなります.
#include <iostream> #include <string> class Rational { public: explicit Rational(int numerator=0, int denominator=1) : m_numerator(numerator), m_denominator(denominator) { std::cout << "Rational(int numerator, int denominator)" << std::endl; } ~Rational() { std::cout << "~Rational()" << std::endl; } Rational(const Rational &rhs) { this->m_numerator = rhs.m_numerator; this->m_denominator = rhs.m_denominator; std::cout << "Rational(const Rational &rhs)" << std::endl; } Rational& operator=(const Rational& rhs) { std::cout << "operator=(const Rational& rhs)" << std::endl; m_numerator = rhs.m_numerator; m_denominator = rhs.m_denominator; return *this; } int m_numerator; int m_denominator; }; int main(int, char**) { std::cout << "chap_4th_24_2.cpp" << std::endl; // コンストラクタが explicit として宣言されたので,下記はコンパイルエラー. //Rational r = 100; // コンストラクタが明示的に呼ばれているので,これはOK! Rational r(100); std::cout << "r.m_numerator : " << r.m_numerator << std::endl; std::cout << "r.m_denominator : " << r.m_denominator << std::endl; return 0; }
上記のサンプルコードでは,Rational r = 100ではもはやコンパイルが通らなくなりました.代わりにコンストラクタを明示的に呼び出した "Rational r(100)" はコンパイルが通ります.
ということで,次回のエントリに続きます...
#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 では,コンパイラが自動生成したデフォルトのコピーコンストラクタが呼ばれているからです.つまり,一般化されたコピーコンストラクタ,コピー代入演算子を宣言・定義しても,コンパイラはそれらの自動生成を抑制しません.そのため,コンパイラがそれらを自動生成することを抑制したければ,”一般化していない”コピーコンストラクタ・コピー代入演算子を宣言・定義する必要があります.
#43 テンプレート化された基底クラスの名前へのアクセス方法
Effective C++ 43項の内容です.
テンプレート化された基底クラスの名前は,派生クラスからは見えない.
ということで,サンプルコードを見ていきます.まず,下記のような基底クラスと派生クラスがあったとします.
class ElementA { public: void sayType() { std::cout << "ElementA" << std::endl; } }; class ElementB { public: }; template<typename ElemType> class BaseStorage { public: void sayType() { ElemType e; e.sayType(); } }; /** これはNG template<typename ElemType> class InheritedStorageANG : public BaseStorage <ElemType> { public: void clarifyType() { sayType(); } }; **/
今までのルールで言うと,InheritedStorageANGの sayType() 呼び出しは,派生クラス名前が隠蔽されていないのででコンパイルが通るはずです.が,今回のケースではコンパイルが通りません.これがまさに今回の "テンプレート化された基底クラスの名前は,派生クラスからは見えない." という内容になります.なぜ C++ のルールがこういうふうになっているかというと,sayType() を持たない BaseStorage
どういうことかというと,BaseStorage はテンプレート特化させて下記のように複数の定義を持つことが可能です.
class ElementA { public: void sayType() { std::cout << "ElementA" << std::endl; } }; class ElementB { public: }; template<typename ElemType> class BaseStorage { public: void sayType() { ElemType e; e.sayType(); } }; // テンプレート特化を使うと,sayTypeを持たないBaseStorageの定義が可能になる. template<> class BaseStorage<ElementB> { };
上記のサンプルでわかることは,テンプレートパラメータの値によって,BaseStorageの中身が変わるということです.つまり,テンプレートパラメータの値が確定していない時点で,派生クラスに sayType() があるかどうかわからないわけです.ではでは,どのように基底クラスのメソッドやメンバを呼びだせばいいかという話ですが,呼び出す方法は3つあります.
1.基底クラスの関数呼び出しの前に this-> をつける.
template<typename ElemType> class InheritedStorageA1 : public BaseStorage <ElemType> { public: void clarifyType() { this->sayType(); } };
2.派生クラスで using を使って,名前が存在することを明示する.
template<typename ElemType> class InheritedStorageA2 : public BaseStorage <ElemType> { public: using BaseStorage<ElemType>::sayType; void clarifyType() { sayType(); } };
3.派生クラスの関数呼び出しで,関数名をきちんと修飾する.
template<typename ElemType> class InheritedStorageA3 : public BaseStorage <ElemType> { public: void clarifyType() { BaseStorage<ElemType>::sayType(); } };
ということで,今日のサンプルコードです.コンパイルエラーになる箇所は,コメントアウトしています.
#include <iostream> class ElementA { public: void sayType() { std::cout << "ElementA" << std::endl; } }; class ElementB { public: }; template<typename ElemType> class BaseStorage { public: void sayType() { ElemType e; e.sayType(); } }; // テンプレート特化を使うと,sayTypeを持たないBaseStorageの定義が可能になる. template<> class BaseStorage<ElementB> { }; /** これはNG template<typename ElemType> class InheritedStorageANG : public BaseStorage <ElemType> { public: void clarifyType() { sayType(); } }; **/ template<typename ElemType> class InheritedStorageA1 : public BaseStorage <ElemType> { public: void clarifyType() { this->sayType(); } }; template<typename ElemType> class InheritedStorageA2 : public BaseStorage <ElemType> { public: using BaseStorage<ElemType>::sayType; void clarifyType() { sayType(); } }; template<typename ElemType> class InheritedStorageA3 : public BaseStorage <ElemType> { public: void clarifyType() { BaseStorage<ElemType>::sayType(); } }; int main(int, char**) { std::cout << "chap_7th_43_1.cpp" << std::endl; /** これはNG! InheritedStorageANG<ElementA> storageANG; storageANG.clarifyType(); **/ InheritedStorageA1<ElementA> storageA1; storageA1.clarifyType(); InheritedStorageA2<ElementA> storageA2; storageA2.clarifyType(); InheritedStorageA3<ElementA> storageA3; storageA3.clarifyType(); /** これはNG! InheritedStorageA3<ElementB> storageA3_B; storageA3_B.clarifyType(); **/ return 0; }
おまけ.
上記の例は,オブジェクトファイルを生成する時点でコンパイラが BaseStorage に sayType() が存在するかどうかわからないことから発生します.逆に,下記のような書き方だとコンパイルエラーになりません.
#include <iostream> class ElementA { public: void sayType() { std::cout << "ElementA" << std::endl; } }; template<typename ElemType> class BaseStorage { public: void sayType() { ElemType e; e.sayType(); } }; // この時点でコンパイラは BaseStorage のテンプレートパラメータが ElementA ということがわかるので,基底クラスに sayType() が存在することを保証できる. class InheritedStorageA : public BaseStorage <ElementA> { public: void clarifyType() { sayType(); } }; int main(int, char**) { std::cout << "chap_7th_43_2.cpp" << std::endl; InheritedStorageA storageA; storageA.clarifyType(); return 0; }
#33 継承した名前を隠蔽しない
Effective C++ 33項の内容です.
基底クラスにある名前は,派生クラスに同じ名前があると隠蔽される.
基底クラスにある名前は,派生クラスで同じ名前が使われるとアクセスできなくなります.下記,サンプルコードです.
#include <iostream> class Base { public: class Internal { public: Internal() { std::cout << "Base::Internal" << std::endl; } }; void methodA() { std::cout << "Base::methodA()" << std::endl; }; void methodB(int a) { std::cout << "Base::methodB()" << std::endl; }; void methodC() { std::cout << "Base::methodC()" << std::endl; }; }; class Inherited : public Base { public: class Internal { public: Internal() { std::cout << "Inherited::Internal" << std::endl; } }; void methodA() { std::cout << "Inherited::methodA()" << std::endl; }; void methodB() { std::cout << "Inherited::methodB()" << std::endl; }; public: int methodC; }; int main(int, char**) { std::cout << "" << std::endl; Inherited obj; obj.methodA(); //obj.methodB(0); // 基底クラスで宣言されているmethodB(int)は隠蔽されるため,アクセス不可! obj.methodB(); //obj.methodC(); // 基底クラスで宣言されているmethodC()は隠蔽されるため,アクセス不可! Inherited::Internal intl; // 基底クラスで定義されている内部クラスがインスタンス化される. return 0; }
上記の例で何種類か例を上げましたが,隠蔽は純粋に名前だけに関して行われます.例えばmethodBの場合,親クラスでの定義と子クラスでの定義ではメソッドの引数が異なりますが,やはりアクセスできなくなります.methodCの場合,Baseクラスではメソッドとして,Inheritedクラスでは変数として宣言されていますが,ここでは純粋に名前だけに基づいて隠蔽されるので,派生クラスからはmethodC()は見えなくなっています.
usingを使って隠蔽された名前を可視にする.
それでは,どうやって隠蔽された関数に派生クラスのオブジェクトからアクセスできるようにするのか?という話ですが,"using",もしくは"仕事を送る関数"を使えば可視になります.
usingを使って隠蔽された関数を可視にしたサンプル.
#include <iostream> class Base { public: class Internal { public: Internal() { std::cout << "Base::Internal" << std::endl; } }; void methodA() { std::cout << "Base::methodA()" << std::endl; }; void methodB(int a) { std::cout << "Base::methodB()" << std::endl; }; void methodC() { std::cout << "Base::methodC()" << std::endl; }; }; class Inherited : public Base { public: using Base::methodA; using Base::methodB; //using Base::methodC; //using Base::Internal; class Internal { public: Internal() { std::cout << "Inherited::Internal" << std::endl; } }; void methodA() { std::cout << "Inherited::methodA()" << std::endl; }; void methodB() { std::cout << "Inherited::methodB()" << std::endl; }; public: int methodC; }; int main(int, char**) { std::cout << "" << std::endl; Inherited obj; obj.methodA(); obj.methodB(0); // 基底クラスで宣言されているmethodB(int)は隠蔽されるため,アクセス不可! obj.methodB(); //obj.methodC(); // usingを使っても,変数と名前がぶつかるのでコンパイルエラー. Inherited::Internal intl; // 基底クラスで定義されている内部クラスがインスタンス化される. return 0; }
実行結果
Inherited::methodA() Base::methodB() Inherited::methodB() Inherited::Internal
上の例ではアクセスできなかった Base::methodB が派生クラスのオブジェクトからアクセスできる様になりました.ただし,methodAの例を見てもわかる通り,usingを使っても同名の関数が派生クラスで定義されていれば,やっぱり呼ばれるのは派生クラスでオーバーライドされた関数になります.また,using methodC の例では,関数の名前を可視にしてもクラスの中に同名の変数が定義されているのでコンパイルエラーになりました.
仕事を送る関数を派生クラスに定義する.
隠蔽されてしまった名前に派生クラスオブジェクトのユーザがアクセスする方法として,もうひとつ”仕事を送る関数”を定義する.という方法もあります.
#include <iostream> class Base { public: void methodA() { std::cout << "Base::methodA()" << std::endl; }; void methodB(int a) { std::cout << "Base::methodB()" << std::endl; }; }; class Inherited : public Base { public: void methodA() { std::cout << "Inherited::methodA()" << std::endl; }; void methodB() { std::cout << "Inherited::methodB()" << std::endl; }; void BaseMethodA() { Base::methodA(); }; void BaseMethodB(int a) {Base::methodB(a); }; public: }; int main(int, char**) { std::cout << "" << std::endl; Inherited obj; obj.methodA(); obj.BaseMethodA(); obj.methodB(); obj.BaseMethodB(1); return 0; }
実行結果
Inherited::methodA() Base::methodA() Inherited::methodB() Base::methodB()
上の例では,BaseMethodA, BaseMethodBという仕事を送る関数を定義しています.派生クラス内部で,"親クラス名::名前"として基底クラスの関数にアクセスしています.
#42 typenameの2つの意味を理解する.
Effective C++ 42項の内容です.
typenameの2つの用法
この項で述べられているtypenameの2つの意味ですが,下記です.
1. テンプレートパラメータの宣言として使う.
2. 「C++では,テンプレートパラメータ内部にあるネストされた依存型名はそのままでは型名と解釈されない」ため,型名として指示するために使う.
なんだかよくわからない日本語になってしまいましたが,いつもの如くサンプルコードで実験していきます.
1. テンプレートパラメータの宣言として使う.
まずは自明な?というか,一番ベーシックなtypenameの使い方である,「テンプレートパラメータとしての使用」です.STLのベクターみたいに,クラスや関数などが扱う型をintとかdoubleとか予めかっちり決めるのではなく,ライブラリを使う人が決めることができます.サンプルコードは2つ書きましたが,この使い方の場合は,typenameを使っても,classを使ってもどちらでもOKです.
template<class T> class TemplatedClass1 { public: T member; }; template<typename T> class TemplatedClass2 { public: T member; }; int main(int, char**) { TemplatedClass1<int> tc1; TemplatedClass2<int> tc2; return 0; }
2. 「C++では,テンプレートパラメータ内部にあるネストされた依存型名はそのままでは型名と解釈されない」ため,型名として指示するために使う.
わけわからない日本語になってしまってますが,サンプルコードをまず書きます.
#include <iostream> class TypeClass { public: enum value_type { val1 = 100, val2 }; // <-- テンプレートパラメータ内部にある,ネストされた依存型名. static const int value_type = 0; }; template<typename T> class TemlatedClass { public: void print() { //T::value_type *val1; typename T::value_type val1 = T::value_type::val1; // <-- テンプレートパラメータ内部にある,ネストされた依存型名はそのままでは型名と解釈されないので,typenameをつける. int val2 = T::value_type; std::cout << val1 << std::endl; std::cout << val2 << std::endl; } }; int main(int, char**) { std::cout << "chap_7th_42_1.cpp" << std::endl; TemlatedClass<TypeClass> tc; tc.print(); return 0; }
上記の例の様に,テンプレートパラメータとして渡されるクラス「TypeClass」の内部で,下記の2つが同時に宣言されています.
・enum value_type という型.
・static const int value_typeという静的定数変数.
この時,このクラスをテンプレートパラメータとして使う TemplatedClass を考えてみます.ここで,上記のサンプルコードにprint()という関数を作りましたが,コメントアウトした下記の表記を見てみます.
//T::value_type *val1; <- この書き方はアウト!value_typeがTypeClassの静的変数のことをいっているのか,内部定義されたenumのことを言っているのかわからない.
T::value_type の解釈の仕方が2つあります.
1.テンプレート型の内部で宣言されている enum の value_type
2.テンプレート型の内部で宣言されている静的定数変数の value_type
1の解釈で一行を読み取ると,val1はenum変数のポインタ.2の解釈で一行を読み取ると,掛け算をしている行になります.
ということで,この曖昧さを取り除くために,「ネストされた依存名の型を使って宣言したい場合は,先頭に typename をつける.」となります.
ちなみに,上記のコードを実行すると下記の所望の実行結果が得られていることがわかります.
chap_7th_42_1.cpp 100 <- enum value_type::val1の値が出ている. 0 <- 静的定数変数 TypeClass::value_typeの値が出ている.