Multiple inheritance pointer comparison

2020-08-09 10:19发布

问题:

I have a class Derived that inherits directly from two base classes, Base1 and Base2. I'd like to know if it's safe, in general, to compare pointers to the base classes to determine if they are the same Derived object:

Base1* p1;
Base2* p2;

/*
 * Stuff happens here. p1 and p2 now point to valid objects of either their
 * base type or Derived
 */

//assert(p1 == p2); //This is illegal
assert(p1 == static_cast<Base1*>(p2)); //Is this ok?
assert(static_cast<Derived*>(p1) == static_cast<Derived*>(p2)); //How about this?

The pointers are guaranteed to be valid, but not necessarily to point to a Derived object. My guess is that this is probably fine, but I wanted to know if it was ok from a technical C++ perspective. I actually never do any operations on the pointers, I just want to know if they point to the same object.

EDIT: It seems to be safe if I can guarantee that p1 and p2 point to Derrived objects. I basically want to know if it is safe if they don't- if one or both point to a base object, will the comparison necessarily fail? Again, I can guarantee the pointers are valid (i.e., p1 would never point at a Base2 object or vice versa)

回答1:

Well, no, it won't work.

I'm personally a big fan of learning-by-example, so here's one:

#include <iostream>

class Base1
{
public:
    Base1()
    {
        numberBase1 = 1;
    }

    int numberBase1;
};

class Base2
{
public:
    Base2()
    {
        numberBase2 = 2;
    }

    int numberBase2;
};

class Derived : public Base1, public Base2
{
public:
    Derived()
    {
        numberDerived = 3;
    }

    int numberDerived;
};

int main()
{
    Derived d;
    Base1 *b1 = &d;
    Base2 *b2 = &d;

    std::cout << "d: " << &d << ", b1: " << b1 << ", b2: " << b2 << ", d.numberDerived: " << &(d.numberDerived) << std::endl;

    return 0;
}

One run-through on my computer outputted this:

d: 0035F9FC, b1: 0035F9FC, b2: 0035FA00, d.numberDerived: 0035FA04

Soo.. If we define the address of d as 0, then b1 is 0, b2 is +4 and the number of d is +8. This is because an int on my machine is 4 byte long.

Basically, you have to look at the layout of how C++ internally represents a class:

Address:    Class:
0           Base1
4           Base2
8           Derived

.. So in total, instantiating a Derived class will allocate space for the base classes of the derived class, and finally make room for the derived object itself. Since we have 3 integers here, that'll be 12 bytes.

Now, what you're asking (unless I misunderstood something) is if you can compare the address of the different base class pointers to each other to see if they point to the same object, and the answer is no - Not directly at least, as in my example, b1 would point to 0035F9FC, while b2 would point to 0035FA00. In C++, this offsetting is all done at compile time.

You could probably do some magic with RIIA and sizeof() and determine how much of an offset b2 should have to be comparable to b1, but then you run into all kinds of other trouble like virtuals. In short, I would not recommend this approach.

A much better way would be to cast to Derived* like ialiashkevich said, however, that would impose a problem if your object was not an instance of Derived*.

(Disclaimer; I haven't used C++ in 3-4 years, so I might be a bit off my game. Be gentle :) )



回答2:

Casting to Derived* before comparison is the right way to go.

There is a similar topic: C++ pointer multi-inheritance fun



回答3:

The short answer is no, this is generally not a good idea.

NOTE: This is assuming you want custom equivalence for all of your classes, if you want to check if they are the same object, it is better to do (Derived *).

A much better solution would be to overload the == operator for Base1, Base2, and Derived.

Assuming Base1 has 1 parameter param1 for equality and Base2 has another parameter param2 for equality:

virtual bool Base1::operator==(object& other){
    return false;
}

virtual bool Base1::operator==(Base1& other)
{
    return this.param1 == other.param1;
}

virtual bool Base2::operator==(object& other){
    return false;
}

virtual bool Base2::operator==(Base2& other)
{
    return this.param2 == other.param2;
}

virtual bool Derived::operator==(object& other){
    return false;
}

virtual bool Derived::operator==(Derived& other){
    return this.param1 == other.param1 && this.param2 == other.param2;
}

virtual bool Derived::operator==(Base1& other){
    return this.param1 == other.param1;
}

virtual bool Derived::operator==(Base2& other){
    return this.param2 == other.param2;
}


回答4:

Well, turns out the shortest way to achieve what you're looking for is:

assert(dynamic_cast<void*>(p1) == dynamic_cast<void*>(p2));

Dynamically casting to void* effectively down-casts the given pointer to its most derived class, so you're guaranteed if both point on the same object, the assert won't fail.

Indeed, there are practical uses for dynamic-casting to void pointer...

Edit: to answer the question's edit, the comparison is not safe. Consider the following code:

Base2 b2;
Base1 b1;
assert(static_cast<Derived*>(&b1) == static_cast<Derived*>(&b2));  // succeeds!

The memory layout of the two different bases is similar to that of a Derived (on a common implementation - the stack grows opposite of the heap). The first static_cast leaves the pointer as is, but the second one moves the pointer sizeof(Base1) back, so now they both point on &b1, and the assert succeeds - even though the objects are different.

You should use static_cast only if you know for sure the cast is correct. This is not your case, so you must use dynamic_cast, possibly as suggested above.



回答5:

It appears to be invalid, based on this SO question: How is C++'s multiple inheritance implemented?

Basically, because of the way the objects are laid out in memory, a cast to either Base1* or Base2* results in a mutation of the pointer that I can't arbitrarily reverse at runtime without a dynamic_cast, which I'd like to avoid. Thanks everyone!



回答6:

Use dynamic_cast, and watch out for NULL.

#include <cassert>

struct Base1 { virtual ~Base1() {} };
struct Base2 { virtual ~Base2() {} };
struct Derived : Base1, Base2 {};

bool IsEqual(Base1 *p1, Base2 *p2) {
  Derived *d1 = dynamic_cast<Derived*>(p1);
  Derived *d2 = dynamic_cast<Derived*>(p2);

  if( !d1 || !d2 ) return false;
  return d1 == d2;
}

int main () {
  Derived d;
  Base1 *p1 = &d;
  Base2 *p2 = &d;
  Base1 b1;
  Base2 b2;

  assert(IsEqual(p1, p2));
  assert(!IsEqual(p1, &b2));
  assert(!IsEqual(&b1, p2));
  assert(!IsEqual(&b1, &b2));
}


回答7:

assert(p1 == p2);                      //This is illegal
assert(p1 == static_cast<Base1*>(p2)); //Is this ok?
assert(static_cast<Derived*>(p1) 
       == static_cast<Derived*>(p2));  //How about this?

None of them is a good solution. The first one will not compile, as you cannot compare pointers of unrelated types. The second one will not compile either (unless Base1 and Base2 are related through inheritance) for the same reason: you cannot static_cast to a pointer of an unrelated type.

The third option is borderline. That is, it is not correct, but it will work in many cases (as long as inheritance is not virtual).

The proper way of comparing for identity would be using dynamic_cast to the derived type and checking for null:

{
  Derived *tmp = dynamic_cast<Derived*>(p1);
  assert( tmp && tmp == dynamic_cast<Derived*>(p2) );
{