web-dev-qa-db-ja.com

クラス階層のoperator ==をオーバーロードする正しい方法は何ですか?

次のクラス階層があると仮定します。

class A
{
    int foo;
    virtual ~A() = 0;
};

A::~A() {}

class B : public A
{
    int bar;
};

class C : public A
{
    int baz;
};

オーバーロードする正しい方法は何ですかoperator==これらのクラスの場合?すべてを無料の関数にすると、BとCはキャストせずにAのバージョンを活用できません。また、誰かがAへの参照のみを持つ深い比較を行うことを防ぎます。それらを仮想メンバー関数にすると、派生バージョンは次のようになります。

bool B::operator==(const A& rhs) const
{
    const B* ptr = dynamic_cast<const B*>(&rhs);        
    if (ptr != 0) {
        return (bar == ptr->bar) && (A::operator==(*this, rhs));
    }
    else {
        return false;
    }
}

繰り返しますが、私はまだキャストする必要があります(そして、それは間違っていると感じます)。これを行うための好ましい方法はありますか?

更新:

これまでのところ2つの答えしかありませんが、正しい方法は代入演算子に似ているようです。

  • 葉以外のクラスを抽象化する
  • 葉以外のクラスで保護された非仮想
  • リーフクラスのパブリック非仮想

ベース関数が保護されているため、ユーザーが異なるタイプの2つのオブジェクトを比較しようとしてもコンパイルされず、リーフクラスは親のバージョンを活用してデータのその部分を比較できます。

44

このような階層については、Scott MeyerのEffective C++のアドバイスに間違いなく従い、具体的な基本クラスを持たないようにします。いずれにしてもこれをしているように見えます。

operator==を、具体的なリーフノードクラスタイプ専用の無料の関数、おそらくは友人として実装します。

基本クラスにデータメンバーが必要な場合は、派生クラスのoperator==が使用できる(おそらく保護された)非仮想ヘルパー関数を基本クラスに提供します(たとえば、isEqual) 。

例えば。

bool operator==(const B& lhs, const B& rhs)
{
    lhs.isEqual( rhs ) && lhs.bar == rhs.bar;
}

抽象基本クラスで機能するoperator==を持たず、比較関数を保護したままにすることで、2つの異なるタイプのオブジェクトの基本部分のみが比較されるクライアントコードで誤ってフォールバックすることはありません。

dynamic_castを使用して仮想比較関数を実装するかどうかはわかりませんが、これを行うには消極的ですが、必要性が実証されている場合は、おそらくベースに純粋な仮想関数を使用しますclass(notoperator==)。これは、派生クラスにoperator==を使用して、具体的な派生クラスでこのようなものとしてオーバーライドされました。

bool B::pubIsEqual( const A& rhs ) const
{
    const B* b = dynamic_cast< const B* >( &rhs );
    return b != NULL && *this == *b;
}
18
CB Bailey

私は先日同じ問題を抱えていましたが、次の解決策を思いつきました。

struct A
{
    int foo;
    A(int prop) : foo(prop) {}
    virtual ~A() {}
    virtual bool operator==(const A& other) const
    {
        if (typeid(*this) != typeid(other))
            return false;

        return foo == other.foo;
    }
};

struct B : A
{
    int bar;
    B(int prop) : A(1), bar(prop) {}
    bool operator==(const A& other) const
    {
        if (!A::operator==(other))
            return false;

        return bar == static_cast<const B&>(other).bar;
    }
};

struct C : A
{
    int baz;
    C(int prop) : A(1), baz(prop) {}
    bool operator==(const A& other) const
    {
        if (!A::operator==(other))
            return false;

        return baz == static_cast<const C&>(other).baz;
    }
};

私がこれについて気に入らないのは、typeidチェックです。あなたはそれについてどう思いますか?

12
Job

両方のオブジェクトのタイプが等しくなければならないという合理的な仮定を立てる場合、各派生クラスで必要なボイラープレートの量を減らす方法があります。これは Herb Sutterの推奨 に従い、仮想メソッドを保護し、パブリックインターフェイスの背後に隠します。 奇妙な繰り返しテンプレートパターン(CRTP) は、equalsメソッドにボイラープレートコードを実装するために使用されるため、派生クラスは必要ありません。

class A
{
public:
    bool operator==(const A& a) const
    {
        return equals(a);
    }
protected:
    virtual bool equals(const A& a) const = 0;
};

template<class T>
class A_ : public A
{
protected:
    virtual bool equals(const A& a) const
    {
        const T* other = dynamic_cast<const T*>(&a);
        return other != nullptr && static_cast<const T&>(*this) == *other;
    }
private:
    bool operator==(const A_& a) const  // force derived classes to implement their own operator==
    {
        return false;
    }
};

class B : public A_<B>
{
public:
    B(int i) : id(i) {}
    bool operator==(const B& other) const
    {
        return id == other.id;
    }
private:
    int id;
};

class C : public A_<C>
{
public:
    C(int i) : identity(i) {}
    bool operator==(const C& other) const
    {
        return identity == other.identity;
    }
private:
    int identity;
};

http://ideone.com/SymduV でデモをご覧ください

9
Mark Ransom

キャストを使用したくなく、BのインスタンスとCのインスタンスを誤って比較しないことを確認する場合、スコットマイヤーズがMore Effective C++の項目33で提案する方法でクラス階層を再構築する必要があります。実際、このアイテムは代入演算子を扱います。これは、関連のないタイプに使用する場合、実際には意味がありません。比較操作の場合、BのインスタンスをCと比較するときにfalseを返すのは理にかなっています。

以下は、RTTIを使用し、クラス階層をconcreateリーフと抽象ベースに分割しないサンプルコードです。

このサンプルコードの良い点は、非関連インスタンス(BとCなど)を比較するときにstd :: bad_castを取得しないことです。それでも、コンパイラーは必要に応じてそれを行うことができます。同じ方法でoperator <を実装し、さまざまなA、B、Cインスタンスのベクトルのソートに使用できます。

ライブ

#include <iostream>
#include <string>
#include <typeinfo>
#include <vector>
#include <cassert>

class A {
    int val1;
public:
    A(int v) : val1(v) {}
protected:
    friend bool operator==(const A&, const A&);
    virtual bool isEqual(const A& obj) const { return obj.val1 == val1; }
};

bool operator==(const A& lhs, const A& rhs) {
    return typeid(lhs) == typeid(rhs) // Allow compare only instances of the same dynamic type
           && lhs.isEqual(rhs);       // If types are the same then do the comparision.
}

class B : public A {
    int val2;
public:
    B(int v) : A(v), val2(v) {}
    B(int v, int v2) : A(v2), val2(v) {}
protected:
    virtual bool isEqual(const A& obj) const override {
        auto v = dynamic_cast<const B&>(obj); // will never throw as isEqual is called only when
                                              // (typeid(lhs) == typeid(rhs)) is true.
        return A::isEqual(v) && v.val2 == val2;
    }
};

class C : public A {
    int val3;
public:
    C(int v) : A(v), val3(v) {}
protected:
    virtual bool isEqual(const A& obj) const override {
        auto v = dynamic_cast<const C&>(obj);
        return A::isEqual(v) && v.val3 == val3;
    }
};

int main()
{
    // Some examples for equality testing
    A* p1 = new B(10);
    A* p2 = new B(10);
    assert(*p1 == *p2);

    A* p3 = new B(10, 11);
    assert(!(*p1 == *p3));

    A* p4 = new B(11);
    assert(!(*p1 == *p4));

    A* p5 = new C(11);
    assert(!(*p4 == *p5));
}
8
marcinj
  1. これは奇妙に見えると思う:

    void foo(const MyClass& lhs, const MyClass& rhs) {
      if (lhs == rhs) {
        MyClass tmp = rhs;
        // is tmp == rhs true?
      }
    }
    
  2. Operator ==の実装が正当な質問のように思える場合は、型の消去を検討してください(とにかく型の消去を検討してください。これは素晴らしいテクニックです)。 これを説明するSean Parentがいます その後、複数のディスパッチを行う必要があります。それは不快な問題です。 これについての話があります。

  3. 階層の代わりにバリアントを使用することを検討してください。彼らはこの種のことを簡単に行うことができます。

0