Сравнение нескольких указателей наследования
У меня есть класс 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
или наоборот)
Ответы
Ответ 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 лет, так что я, возможно, немного оторвался от игры. Будьте осторожны :))
Ответ 2
Листинг до Derived*
перед сравнением - это правильный путь.
Существует аналогичная тема: Наследование с несколькими наследованиями С++
Ответ 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) );
{