多重継承の問題と適切な使用方法

多重継承の問題:複数の仮想関数テーブルの生成

C++における多重継承にはいくつかの問題点があります。ここでは、多重継承によって複数の仮想関数テーブルが生成される問題について解説します。

#include <iostream>

using namespace std;

class BaseA
{
public:    
    virtual void display()
    {
        cout << "BaseA::display()" << endl;
    }
};

class BaseB
{
public:
    virtual void display()
    {
        cout << "BaseB::display()" << endl;
    }
};

class Derived : public BaseA, public BaseB
{
};                
int main()
{
    cout << "sizeof(Derived) = " << sizeof(Derived) << endl; // 8 ⇒ 2つの仮想関数テーブルへのポインタ

    Derived d;
    BaseA *pa = &d;
    BaseB *pb = &d;

    pa->display(); // BaseA::display()
    pb->display(); // BaseB::func()    
    cout << "pa = " << pa << endl; // pa = 0x...
    cout << "pb = " << pb << endl; // pb = 0x... (paより4バイト大きい)
    
    cout << endl;
 
    BaseB* pbb = (BaseB *)(pa); // 強制的な型変換、ポインタの値は変更されない
    pbb->display(); // BaseB::display()を期待するが、実際はBaseA::display()
    cout << "pbb = " << pbb << endl; // pbと同じアドレスを期待するが、paと同じアドレスになる

    cout << endl;

    BaseB* pbbb = dynamic_cast<BaseB*>(pa); // コンパイラはオブジェクトdのアドレスからオブジェクトを取得し、BaseBを探してポインタの値をBaseBのアドレスに修正する
    pbbb->display(); // BaseB::display()を期待し、実際もそれになる
    cout << "pbbb = " << pbbb << endl; // pbと同じアドレスになる

    return 0;
}

/* 出力結果
sizeof(Derived) = 8
BaseA::display()
BaseB::display()
pa = 0x...
pb = 0x...

BaseA::display()
pbb = 0x...

BaseB::display()
pbbb = 0x...
*/

多重継承の適切な使用:開発におけるベストプラクティス

(1)親クラスを1つ継承し、複数のインターフェースを実装する

(2)親クラスにequals()メンバ関数を提供する

(3)equals()メンバ関数はポインタが現在のオブジェクトを指しているか判断するために使用する

(4)多重継承関連の型変換はdynamic_castで行う

#include <iostream>

using namespace std;

// 親クラス
class Parent
{
protected:
    int value;
public:
    Parent(int v)
    {
        value = v;
    }
    
    int getValue()
    {
        return value;
    }
    
    // ポインタが同じオブジェクトを指しているか判断するためのテクニック
    bool equals(Parent* obj)
    {
        return (this == obj);
    }
};

// インターフェース1
class Interface1
{
public:
    virtual void add(int i) = 0;
    virtual void subtract(int i) = 0;
};

// インターフェース2
class Interface2
{
public:
    virtual void multiply(int i) = 0;
    virtual void divide(int i) = 0;
};

// 見た目は多重継承に似ているが、Parentクラス以外はすべてインターフェース
class Child : public Parent, public Interface1, public Interface2
{
public:
    Child(int v): Parent(v) // 初期化リストで親クラスのコンストラクタを呼び出す
    {
        
    }
    
    void add(int i)
    {
        value += i;
    }
    
    void subtract(int i)
    {
        value -= i;
    }
    
    void multiply(int i)
    {
        value *= i;
    }
    
    void divide(int i)
    {
        if( i != 0)
        {
            value /= i;
        }
    }
};

int main()
{  
    Child c(100);
    Child* p = &c;
    Interface1* pInt1 = &c; // 代入の互換性
    Interface2* pInt2 = &c;
    
    cout << "p->getValue() = " << p->getValue() << endl; // 100
    
    pInt1->add(10);     // value == 110
    pInt2->divide(11);  // value == 110 / 11
    pInt1->subtract(5); // value = 10 - 5
    pInt2->multiply(8); // value = 5 * 8
    
    cout << "p->getValue() = " << p->getValue() << endl; // 40
    
    cout << endl;
    
    // dynamic_castを使ってpInt1とpInt2ポインタをcオブジェクトのParent部分に変換
    // pInt1とpInt2の値は異なるが、同じParentポインタに変換され、
    // Parentクラスのequals関数で変換後の2つのポインタが等しいか判断し、
    // それらが同じオブジェクトcを指しているかどうかを判断できる
    // これにより多重継承での複数のアドレス問題が解決できる
    cout << "pInt1 == p: " << p->equals(dynamic_cast<Parent*>(pInt1)) << endl;  // 1
    cout << "pInt2 == p: " << p->equals(dynamic_cast<Parent*>(pInt2)) << endl;  // 1
    
    return 0;
}
/* 出力結果
p->getValue() = 100
p->getValue() = 40

pInt1 == p: 1
pInt2 == p: 1
*/

まとめ

  • 多重継承では複数の仮想関数テーブルポインタが生成される可能性がある
  • 多重継承関連の型変換はdynamic_castで行う
  • 開発では単一継承と複数インターフェースの方式で多重継承を使用する
  • 親クラスにequals()メンバ関数を用意し、ポインタが現在のオブジェクトを指しているか判断するために使用する

タグ: C++ 多重継承 仮想関数テーブル dynamic_cast インターフェース

7月13日 00:31 投稿