Подтвердить что ты не робот

Сравнение нескольких указателей наследования

У меня есть класс Derived, который наследуется непосредственно от двух базовых классов, Base1 и Base2. Я хотел бы знать, безопасно ли вообще сравнивать указатели на базовые классы, чтобы определить, являются ли они одним и тем же объектом Derived:

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?

Указатели гарантированно действительны, но необязательно указывают на объект Derived. Я предполагаю, что это, вероятно, хорошо, но я хотел знать, было ли это хорошо с технической точки зрения на C++. Я на самом деле никогда не делаю никаких операций над указателями, я просто хочу знать, указывают ли они на один и тот же объект.

РЕДАКТИРОВАТЬ: Кажется, это безопасно, если я могу гарантировать, что p1 и p2 указывают на объекты Derrived. Я в основном хочу знать, если это безопасно, если они не... если один или оба указывают на базовый объект, будет ли сравнение неизбежно неудачным? Опять же, я могу гарантировать, что указатели действительны (т.е. p1 никогда не укажет на объект Base2 или наоборот)

4b9b3361

Ответ 1

Ну, нет, это не сработает.

Я лично большой поклонник обучения на собственном примере, поэтому вот один:

#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;
}

Один прогон на моем компьютере вывел это:

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

Soo.. Если мы определим адрес d как 0, то b1 будет 0, b2 будет +4, а число d будет +8. Это потому, что int на моей машине имеет длину 4 байта.

По сути, вы должны взглянуть на макет того, как C++ внутренне представляет класс:

Address:    Class:
0           Base1
4           Base2
8           Derived

Таким образом, в итоге создание экземпляра класса Derived выделит пространство для базовых классов производного класса и, наконец, освободит место для самого производного объекта. Поскольку здесь у нас есть 3 целых числа, это будет 12 байтов.

Теперь, что вы спрашиваете (если я что-то не так понял), если вы можете сравнить адреса различных указателей базового класса друг с другом, чтобы увидеть, указывают ли они на один и тот же объект, и ответ нет - по крайней мере, не напрямую, как в моем примере, b1 будет указывать на 0035F9FC, а b2 будет указывать на 0035FA00. В C++ это смещение выполняется во время компиляции.

Возможно, вы могли бы поработать с RIIA и sizeof() и определить, какая часть смещения b2 должна быть сравнима с b1, но затем вы столкнетесь со всеми другими проблемами, такими как виртуальные. Короче я бы не рекомендовал такой подход.

Гораздо лучшим способом было бы приведение к Derived *, как сказал ialiashkevich, однако, это наложило бы проблему, если бы ваш объект не был экземпляром Derived *.

(Отказ от ответственности; я не использовал C++ в течение 3-4 лет, так что я, возможно, немного оторвался от игры. Будьте осторожны :))

Ответ 3

Короткий ответ - нет, обычно это не очень хорошая идея.

ПРИМЕЧАНИЕ. Предполагается, что для всех ваших классов требуется настраиваемая эквивалентность, если вы хотите проверить, являются ли они одним и тем же объектом, лучше сделать (Derived *).

Лучшим решением было бы перегрузить оператор == для Base1, Base2 и Derived.

Предполагая, что Base1 имеет 1 параметр param1 для равенства, а Base2 имеет другой параметр param2 для равенства:

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

Ну, получается самый короткий способ добиться того, что вы ищете:

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

Динамическое отбрасывание до void* эффективно снижает заданный указатель на его наиболее производный класс, поэтому вам гарантировано, что если обе точки на одном и том же объекте, утверждение не будет терпеть неудачу.

Действительно, существуют практические применения для динамического каста для указателя void...

Изменить: чтобы ответить на вопрос редактирования, сравнение не безопасно. Рассмотрим следующий код:

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

Макет памяти двух разных баз похож на макет Derived (при общей реализации - стек растет напротив кучи). Первый static_cast оставляет указатель как есть, но второй перемещает указатель sizeof(Base1) назад, так что теперь они оба указывают на &b1, и утверждение завершается успешно, даже если объекты разные.

Вы должны использовать static_cast только в том случае, если вы точно знаете, что бросок правильный. Это не ваш случай, поэтому вы должны использовать dynamic_cast, возможно, как было предложено выше.

Ответ 5

Как представляется, это неверно, на основе этого вопроса SO: Как реализовано множественное наследование С++?

В основном, из-за того, как объекты выложены в памяти, приведение к Base1* или Base2* приводит к мутации указателя, который я не могу произвольно изменить во время выполнения без dynamic_cast, которого я бы хотел избежать. Спасибо всем!

Ответ 6

Используйте dynamic_cast и следите за 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?

Ни одно из них не является хорошим решением. Первый не будет компилироваться, поскольку вы не можете сравнивать указатели несвязанных типов. Второй не будет компилироваться (если только Base1 и Base2 связаны по наследованию) по той же причине: вы не можете static_cast указателю несвязанного типа.

Третий вариант - пограничный. То есть, это неверно, но он будет работать во многих случаях (пока наследование не является виртуальным).

Правильный способ сравнения для идентификации будет использовать dynamic_cast для производного типа и проверять значение null:

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