🍎

【C++】派生クラスの区別の仕方

2021/05/06に公開

概要

書籍テスト駆動開発の書籍をC++で実装していた際に、DollarクラスとFrancクラスを判定できるようにする必要があったのですが、その方法がわからなかったので調べた内容をメモとして残します。

本記事は学習中の人が書いているので、誤りなどあればご指摘お願いします。
ソースコード全文は最後のおまけに記載しています。記事途中のコードは抜粋です。

基底クラスと派生クラスとは

基底クラス

親クラス、スーパークラスとも呼ばれるクラス。
同一の概念で設計される複数のクラスの基になるクラスです。
同じことをする処理でも異なる概念の場合は基底クラスにしない方が良いです。
例:動く(move)、走る(run)という概念で、派生クラスである「人間クラス」と「車クラス」に基底クラスの「動く物体クラス」を作るなど

派生クラス

子クラス、サブクラスとも呼ばれるクラス。
基底クラスから作られるクラスで、基底クラスのメンバやメソッドを引き継ぐことができる。

問題と解決策

問題

関数の引数を基底クラス(Base)として、引数に設定された派生クラス(Derived_A,Derived_B)同士を比較した場合に異なるオブジェクトと判定できるようにする。

解決策

引数をポインタとし、比較はポインタの指す実際のオブジェクト同士を比較するようにする。

解説

解説1:基底クラスのデストラクタは仮想関数にすること

class Base {
public:
    Base(int number) : member(number) {}
    virtual ~Base() {};

今回実装する上で重要なのはデストラクタを仮想関数にすることです。
仮想関数にしていないと今回の目的の動作は確認できません。
「virtusl」を消すと全て基底クラスとして認識されてしまいます。

解説2:等価演算子オーバーロード

    bool operator==(const Base& rhs) {
        return member == rhs.member
                && typeid(*this) == typeid(rhs);
    }

等価演算子では、thisポインタの実体と参照先の型を比較することで、クラスが異なる場合は、memberの値が等しくても不一致となるように実装しています。

解説3:引数はポインタにすること

bool equals(Base a, Base b) {
    std::cout << "a : " << typeid(a).name() << std::endl;
    std::cout << "b : " << typeid(b).name() << std::endl;
    return a == b;
}

上記コードは引数をポインタにしなかった場合です。
この場合は派生クラス->基底クラスのアップキャストとなり、基底クラスとして動作してしまいます。
ポインタ、参照で受け取る場合は、ポインタ、参照の型は基底クラスですが、ポインタが指す実体のオブジェクトはそれぞれの派生クラスの状態を保持していますので、型の比較を行うことができます。

解説4:比較する型を間違えない

[equals    ]
(dera_1, dera_2)
a : 4Base
b : 4Base
1
(dera_1, derb_1)
a : 4Base
b : 4Base
1 // ×:基底クラスにアップキャストされるため、一致と判定される。

[equals_ptr]
(dera_1, dera_2)
a  : P4Base
*a : 9Derived_A
b  : P4Base
*b : 9Derived_A
1
(dera_1, derb_1)
a  : P4Base
*a : 9Derived_A
b  : P4Base
*b : 9Derived_B
0 // ○:ポインタの実体を比較したため、不一致と判定される。

[equals_ref]
(dera_1, dera_2)
a  : 9Derived_A
&a : P4Base
b  : 9Derived_A
&b : P4Base
1
(dera_1, derb_1)
a  : 9Derived_A
&a : P4Base
b  : 9Derived_B
&b : P4Base
0 // ○:参照の実体を比較したため、不一致と判定される。

上記は実行結果です。比較対象を間違えると基底クラスの型となってしまうので、「ポインタ」と「参照」それぞれで実体を指す記述の仕方を間違えないようにしましょう。

感想

今回の内容を調査しているときに、「ポリモーフィズムを実現する上で、実際に扱っているクラスを意識する必要があるコードは設計ミスの可能性がある。」というような内容の記事を見つけました。
ポリモーフィズムという言葉の理解もまだまだ足りていないですが、ユーザーがクラス間の違いを意識せずに操作できるようにするという概念で考えるとあまり良くないのかもしれません。
ただし、今回はオブジェクトの比較であり、ユーザーは等価演算子はクラスを意識せずに使えるようになっているので問題ないと考えています。

おまけ

ソースコード

main.cpp
#include <iostream>
#include <typeinfo>

class Base {
public:
    Base(int number) : member(number) {}
    virtual ~Base() {};

    bool operator==(const Base& rhs) {
        return member == rhs.member
                && typeid(*this) == typeid(rhs);
    }

    bool operator!=(const Base& rhs) {
        return !(*this == rhs);
    }

protected:
    const int member;
};

class Derived_A : public Base {
public:
    Derived_A(int number) : Base(number) {}
};

class Derived_B : public Base {
public:
    Derived_B(int number) : Base(number) {}
};

bool equals(Base a, Base b) {
    std::cout << "a : " << typeid(a).name() << std::endl;
    std::cout << "b : " << typeid(b).name() << std::endl;
    return a == b;
}

bool equals_ptr(Base* a, Base* b) {
    std::cout << "a  : " << typeid(a).name() << std::endl;
    std::cout << "*a : " << typeid(*a).name() << std::endl;
    std::cout << "b  : " << typeid(b).name() << std::endl;
    std::cout << "*b : " << typeid(*b).name() << std::endl;
    return *a == *b;
}

bool equals_ref(Base& a, Base& b) {
    std::cout << "a  : " << typeid(a).name() << std::endl;
    std::cout << "&a : " << typeid(&a).name() << std::endl;
    std::cout << "b  : " << typeid(b).name() << std::endl;
    std::cout << "&b : " << typeid(&b).name() << std::endl;
    return a == b;
}

int main() {
    Base base_1(5);
    Base base_2(5);
    Base base_3(6);

    std::cout << "base_1 == base_2 : " << (base_1 == base_2) << std::endl;
    std::cout << "base_1 == base_3 : " << (base_1 == base_3) << std::endl;

    std::cout << std::endl;

    Derived_A dera_1(5);
    Derived_A dera_2(5);
    Derived_A dera_3(6);
    Derived_B derb_1(5);

    std::cout << "dera_1 == dera_2 : " << (dera_1 == dera_2) << std::endl;
    std::cout << "dera_1 == dera_3 : " << (dera_1 == dera_3) << std::endl;
    std::cout << "dera_1 == derb_1 : " << (dera_1 == derb_1) << std::endl;

    std::cout << std::endl;

    std::cout << "[equals    ]" << std::endl;
    std::cout << "(dera_1, dera_2)" << std::endl;
    std::cout << equals(dera_1, dera_2) << std::endl;
    std::cout << "(dera_1, derb_1)" << std::endl;
    std::cout << equals(dera_1, derb_1) << std::endl;
    std::cout << std::endl;
    std::cout << "[equals_ptr]" << std::endl;
    std::cout << "(dera_1, dera_2)" << std::endl;
    std::cout << equals_ptr(&dera_1, &dera_2) << std::endl;
    std::cout << "(dera_1, derb_1)" << std::endl;
    std::cout << equals_ptr(&dera_1, &derb_1) << std::endl;
    std::cout << std::endl;
    std::cout << "[equals_ref]" << std::endl;
    std::cout << "(dera_1, dera_2)" << std::endl;
    std::cout << equals_ref(dera_1, dera_2) << std::endl;
    std::cout << "(dera_1, derb_1)" << std::endl;
    std::cout << equals_ref(dera_1, derb_1) << std::endl;
}

実行結果

base_1 == base_2 : 1
base_1 == base_3 : 0

dera_1 == dera_2 : 1
dera_1 == dera_3 : 0
dera_1 == derb_1 : 0

[equals    ]
(dera_1, dera_2)
a : 4Base
b : 4Base
1
(dera_1, derb_1)
a : 4Base
b : 4Base
1 // ×:基底クラスにアップキャストされるため、一致と判定される。

[equals_ptr]
(dera_1, dera_2)
a  : P4Base
*a : 9Derived_A
b  : P4Base
*b : 9Derived_A
1
(dera_1, derb_1)
a  : P4Base
*a : 9Derived_A
b  : P4Base
*b : 9Derived_B
0 // ○:ポインタの実体を比較したため、不一致と判定される。

[equals_ref]
(dera_1, dera_2)
a  : 9Derived_A
&a : P4Base
b  : 9Derived_A
&b : P4Base
1
(dera_1, derb_1)
a  : 9Derived_A
&a : P4Base
b  : 9Derived_B
&b : P4Base
0 // ○:参照の実体を比較したため、不一致と判定される。

Discussion