派生クラスを扱うときの留意事項と decltype で得られる型 - C++ プログラミング
PROGRAM
.auto-style1 {
white-space: nowrap;
}
.auto-style2 {
vertical-align: middle;
white-space: nowrap;
}
派生クラスを扱うときの留意事項
C++ のクラスはインスタンスを値として直接使ったり、参照やポインタを通して使ったりできます。
クラスの定義方法については クラスを定義する に記してあります。
たとえば "CMyClass" というクラスが定義されていたとします。
これを、インスタンスを直接扱う変数 value を定義したい場合は、次のようにします。
CMyClass value;
インスタンスのポインタや参照を扱いたい場合は、それぞれ次のようにします。
CMyClass* pointer;
CMyClass& reference;
メンバ関数の呼び方はそれぞれ違ってきたりはしますけど、それぞれ次のように記載します。
value.method();
pointer->method();
reference->method();
継承を伴わない簡単なクラスであれば、値の定義もメンバ関数の呼び出しも、悩むことはないと思います。
派生クラスのポインタを扱う場合
ただ、扱うクラスがあるクラスの派生クラスで、変数の型を基底クラスで宣言している場合には、混乱しがちなところが出てきます。
たとえば、"CMyClass" から派生した "CMyClass2" というクラスがあった場合、基底クラスのポインタへ派生クラスのポインタを代入するということは、オブジェクト指向のプログラミングでは有名な技法のひとつです。
CMyClass* p = new CMyClass();
このようにすることで、仮想関数をオーバーライドする という方法を使って、CMyClass も CMyClass2 も一緒くたにして操作することができます。
オーバーライドされた関数であれば、CMyClass 型のポインタを経由しても、代入されているポインタに応じて、CMyClass または CMyClass2 の適切な方の関数が呼び出されます。
ただ、これをポインタを使わないで、値で直接操作をすると話が違ってきます。
CMyClass2 value2;
CMyClass value = value2;
クラスに コピーコンストラクタ が実装されている場合はこのようにして、CMyClass2 の値を CMyClass の変数に代入することはできます。
ただ、これはあくまでも基底の CMyClass のコピーコンストラクタ(または代入演算子)を使って、CMyClass の値を設定しているだけで、CMyClass2 の情報はここで抜け落ちてしまいます。
あくまでも CMyClass のインスタンスを作っていることに注意する必要があります。
もちろん value2 を代入元に指定した value 変数のメンバ関数を呼び出しても、CMyClass の方の関数が呼び出されます。
CMyClass のポインタに格納した場合であれば、継承先である CMyClass2 の方の関数が呼び出されます。
ちなみに、クラスインスタンスの参照を格納する変数であれば、雰囲気はクラスを直接扱う場合とそっくりですけど、内部的には CMyClass2 へのポインタと同じように CMyClass2 のインスタンスをそのまま指しているので、CMyClass2 の方の関数を呼び出すことができます。
CMyClass2 value2;
CMyClass value = value2;
CMyClass* pointer = &value2;
CMyClass& reference = value2;
// CMyClass の方のメソッドが呼び出されます。(value と value2 は別の値です)
value.method();
// CMyClass2 の方のメソッドが呼び出されます。(value2 の関数が呼び出されます)
pointer.method();
// CMyClass2 の方のメソッドが呼び出されます。(value2 の関数が呼び出されます)
reference.method();
派生クラスは、基底クラスと一緒くたにして扱えるのが大きなメリットのひとつですけど、クラスインスタンスを値として直接扱うような場合には、派生元と派生先との関係をよく意識して実装しないと、派生先の情報を落とすことにもつながります。
つまり、派生クラスを扱うことを考えたとき、インスタンスを直接格納する変数を使うと、ずいぶんとその柔軟性が削られてしまうことになってしまいます。
定義する変数の種類と格納されるインスタンス
typeid キーワードで取得できる型情報
定義した変数の型と、そこに代入演算子を使ってインスタンスを代入したときの、その変数で扱われるインスタンスの種類について整理してみたいと思います。
ここでは、次の 2 つのクラスについて、派生側のクラスを基底側のクラスで定義した変数に代入したときの様子を中心に見て行きます。
クラス名 | 基底クラス | 説明 |
---|---|---|
CMyClass | - | 基底クラスです。派生したクラスのインスタンスを受ける変数の型としても使用します。 |
CMyClass2 | CMyClass | CMyClass から派生したクラスです。今回は主に、このインスタンスを CMyClass 型の変数に代入したときの様子を見て行きます。 |
このとき、それぞれのクラス名を typeid の引数に指定した場合の、クラス情報は次の通りになりました。
typeid に指定したもの | 取得できた name 情報 | 説明 |
---|---|---|
CMyClass | 8CMyClass | CMyClass の値型 |
CMyClass2 | 9CMyClass2 | CMyClass2 の値型 |
const CMyClass | 8CMyClass | CMyClass の値型 |
const CMyClass2 | 9CMyClass2 | CMyClass2 の値型 |
CMyClass* | P8CMyClass | CMyClass のポインタ型 |
CMyClass2* | P9CMyClass2 | CMyClass2 のポインタ型 |
CMyClass& | 8CMyClass | CMyClass の値型 |
CMyClass2& | 9CMyClass2 | CMyClass2 の値型 |
このように typeid で取得できる情報は、const 指定や参照指定に影響されず、大きく分けて「値」か「ポインタ」かを得られることができる様子でした。
実際に代入した値の情報を確認する
これを踏まえて、変数に値を代入したときの様子を整理すると、次のようになりました。
変数の型と変数名 | 代入式 | 代入後の変数を typeid して得られた情報 |
説明 | 捕捉 |
---|---|---|---|---|
CMyClass test1 | - | 8CMyClass (CMyClass) |
ディフォルトコンストラクタで生成した CMyClass の値です。 | |
CMyClass2 test2 | - | 9CMyClass2 (CMyClass2) |
ディフォルトコンストラクタで生成した CMyClass2 の値です。 | |
CMyClass value | = test2 | 8CMyClass (CMyClass) |
コピーコンストラクタを使って CMyClass2 の値から生成した CMyClass の値です。 | 生成時に CMyClass のコンストラクタが使われたため、CMyClass2 固有の情報が失われています。 |
CMyClass* pointer | = &test2 | P8CMyClass (CMyClass*) |
CMyClass 型のポインタで CMyClass2 型の値を指すようにしています。 | 型は CMyClass 型のポインタですが、ポインタが指す先の値は CMyClass2 型なので、それをそのまま利用できます。 |
CMyClass& reference | = test2 | 9CMyClass2 (CMyClass2) |
CMyClass 型の参照で CMyClass2 型の値を指すようにしています。 | 変数は通常の値型と同じように使えますが、内部的には別の値への参照なので、ここでは参照先の CMyClass2 がそのまま利用できます。 |
このように、派生クラスを既定と一緒くたにして扱いたいときには、ポインタまたは参照を使ってインスタンスを管理するようにしないと、派生クラスの値を基底クラスの型に格納した時に、派生クラスの情報が抜け落ちてしまう危険性があります。
参照はあくまでも、別のところで確保された値を指すためのものなので、派生クラスも加味してクラスのインスタンスを扱う場合は、最終的にはポインタで全てを管理することになります。
decltype で定義できる変数の型
型推論 の decltype を使ってデータ型を定義する場合も、クラスの変数の型と、そこに代入されているクラスインスタンスによって、定義される変数の扱いが違ってきます。
通常の変数から得られる型
基本的に decltype 自体が認識する型は、元の変数の型の定義に倣う形になるため、typeid で派生先の情報が取れるからといって、decltype が派生先のクラスを示している訳ではありません。
typeid で派生先の情報が取れるのは、単に decltype でポインターか参照として型が得られているためです。
decltype の記載 | 元の変数の型 | 代入されている クラスの型 |
decltype で 定義される型 |
説明 |
---|---|---|---|---|
decltype(value) | CMyClass | CMyClass | CMyClass | 型そのものを示します。代入時にはコピーコンストラクタが呼び出されます。継承クラスを代入しても、decltype に指定した型までしか生成されません。 |
decltype((value)) | CMyClass | CMyClass | CMyClass& | すこし特殊な構文で、クラス型の変数を二重に括弧で括ると、そのクラス型を格納できる参照が得られます。 |
decltype(reference) | CMyClass& | CMyClass2 | CMyClass& | 参照をそのまま維持するため、代入先が派生クラスの場合も継続して派生先を利用できます。 |
decltype(pointer) | CMyClass* | CMyClass2 | CMyClass* | ポインタをそのまま維持するため、代入先が派生クラスの場合も継続して派生先を利用できます。 |
decltype(*pointer) | *(CMyClass*) | CMyClass2 | CMyClass& | ポインタが指す値を decltype に指定すると、その値を格納できる型の参照が得られます。参照なので、代入した変数はそのまま、代入先を利用できます。 |
このように、decltype を使って得られるデータ型は、宣言されている変数の型やそこに代入されている実際の値に従って(忠実に)得られた型になります。
実際には、指定したものが左辺値の場合には、その型の参照 (T&) として宣言されるらしく、通常は decltype で定義した変数に左辺値を代入してもコピーコンストラクタが呼ばれることはありません。
メンバ変数や関数の戻り値から得られる型
クラスインスタンスのメンバ変数や関数の戻り値の場合は少し違って、decltype で取得できる型は次のような感じになります。
decltype に指定するもの | decltype で定義される型 | 説明 |
---|---|---|
インスタンスのメンバ変数 | メンバ変数の型と同じ | 型そのものを示します。そのため、型がクラスの値の場合は、代入時にはコピーコンストラクタが呼び出されます。 |
(インスタンスのメンバ変数) | メンバ変数の型の参照 | インスタンスのメンバ変数でも、括弧に括って指定すると、そのメンバ変数の型の参照を示します。そのため、コピーコンストラクタを介することなく、代入された値そのものを保持できます。 |
値を返す関数 | 戻り値の型の参照 | 戻り値の型の参照を示します。そのため、コピーコンストラクタを介することなく、代入された値そのものを保持できます。 |
左辺値参照を返す関数 | 戻り値の通りの左辺値参照 | 戻り値の通りの型の参照を示します。そのため、コピーコンストラクタを介することなく、代入された値そのものを保持できます。 |
右辺値参照を返す関数 | 戻り値の通りの右辺値参照 | 戻り値の通りの右辺値参照を示します。そのため、指定した関数とは別の変数を使って値を設定するような場合、その値を std::move を使って右辺値参照にして代入する必要があります。代入に当たり、ムーブコンストラクタ等が呼ばれることはありません。 |
ここの、"左辺値参照" や "右辺値参照" というのは、右辺値参照とムーブコンストラクタの使い方 のところで触れてあります。
decltype で得られた型からインスタンスを生成する場合
クラス型を扱う変数の場合、decltype で得られた型を使って、そのクラス型のインスタンスを作成することも可能です。
ポインタではない変数の場合は、とりわけて気を遣うようなところはなくて、decltype(value) をひとつのデータ型と捉えて、それに引数を指定してコンストラクタを呼び出します。
// value と同じ型のクラスが、コピーコンストラクタを使って生成されます。
auto newValue = decltype(value)(value);
// ポインタ型も "*" を使って値として指定すれば、コンストラクタを使って生成できます。
auto newValue = decltype(*pointer)(*pointer);
重要な注意事項として、ポインタを "*" を使って指定した場合、生成されるのはポインタに格納されている実際のクラスの型ではなくて、ポインタ変数を定義した時に指定したクラス型が生成されることになります。
そのため、派生クラスのインスタンスをその基底クラスのポインタ型の変数に代入しているような場合、基底クラスの方の型でインスタンスが生成され、派生クラス固有の情報が捨てられてしまうので注意が必要です。
new 演算子を使ってインスタンスを生成する場合には、いくらかの制約が出てきます。
new 演算子を使って生成する場合、指定できる型は decltype で純粋なクラス型として得られる場合に限られます。得られた型が参照やポインタの場合には new 演算子を使ってインスタンスを作ることはできません。
// 純粋なクラス型を指定した decltype であれば、new 演算子でインスタンスを生成できます。
auto newPointer = new decltype(value)(value);
// 同様に、クラスが持つメンバ変数を使う場合も大丈夫です。
auto newPointer = new decltype(this->m_value)(this->m_value);
ポインタ変数を "*" を使って指定した decltype、すなわち "decltype(*pointer)" を使った場合は、クラスの参照型が得られるため、それを new 演算子に指定することはできません。
[ もどる ]