Как я могу реализовать двойную диспетчеризацию, если я не знаю все классы заранее?
У меня есть базовый класс с (потенциально) большим количеством подклассов, и я хотел бы иметь возможность сравнивать любые два объекта базового класса на равенство. Я пытаюсь сделать это, не вызывая кощунственное ключевое слово typeid.
#include <iostream>
struct Base {
virtual bool operator==(const Base& rhs) const
{return rhs.equalityBounce(this);}
virtual bool equalityBounce(const Base* lhs) const = 0;
virtual bool equalityCheck(const Base* lhs) const = 0;
};
struct A : public Base {
A(int eh) : a(eh) {}
int a;
virtual bool equalityBounce(const Base* lhs) const{
return lhs->equalityCheck(this);
}
virtual bool equalityCheck(const Base* rhs) const {return false;}
virtual bool equalityCheck(const A* rhs) const {return a == rhs->a;}
};
int main() {
Base *w = new A(1), *x = new A(2);
std::cout << (*w == *w) << "\n";
std::cout << (*w == *x) << "\n";
}
Я понимаю, что написанный код не работает, потому что lhs в равенства Bounce() - это Base*, поэтому он даже не знает о версии equalCheck(), которая принимает A*. Но я не знаю, что с этим делать.
3 ответа
Почему это не работает
Проблема с вашей реализацией двойной отправки состоит в том, что вы ожидаете, что наиболее equalityCheck()
называется.
Но ваша реализация полностью основана на полиморфном базовом классе, и equalityCheck(const A*)
перегружает, но не переопределяет equalityCheck(const Base*)
!
Иначе говоря, во время компиляции компилятор знает, что A::equalityBounce()
мог позвонить equalityCheck(A*)
(так как this
является A*
) но к сожалению это вызывает Base::equalityCheck()
который не имеет специализированной версии для A*
параметр.
Как это реализовать?
Чтобы двойная диспетчеризация работала, вам нужно иметь специфичные для типа реализации двойного диспетчерства равенства Check() в вашем базовом классе.
Чтобы это работало, База должна знать о своих потомках:
struct A;
struct Base {
virtual bool operator==(const Base& rhs) const
{
return rhs.equalityBounce(this);
}
virtual bool equalityBounce(const Base* lhs) const = 0;
virtual bool equalityCheck(const Base* lhs) const = 0;
virtual bool equalityCheck(const A* lhs) const = 0;
};
struct A : public Base {
...
bool equalityBounce(const Base* lhs) const override{
return lhs->equalityCheck(this);
}
bool equalityCheck(const Base* rhs) const override {
return false;
}
bool equalityCheck(const A* rhs) const override{
return a == rhs->a;
}
};
Обратите внимание на использование override
чтобы убедиться, что функция действительно переопределяет виртуальную функцию базы.
С этой реализацией это будет работать, потому что:
A::equalityBounce()
позвонюBase::equalityCheck()
- среди всех перегруженных версий этой функции она выберет
Base::equalityCheck(A*)
так какthis
являетсяA*
- вызванный
Base *lhs
объект будет называть егоequalityCheck(A*)
, Еслиlhs
являетсяA*
следовательно, он пойдет наA::equalityCheck(A*)
который даст ожидаемый (правильный) результат. Поздравляю! - предполагать
lhs
будет указатель на другой классX
также получен изBase
, В этом случае,lhs->equalityCheck(A*)
назвал быX::equalityCheck(A*)
и может также вернуть правильный ответ, учитывая, что вы бы сравнитьX
сA
,
Как сделать его расширяемым? Карта двойной отправки!
Проблема двойной диспетчеризации с использованием строго типизированного языка заключается в том, что "отскочившему" объекту необходимо знать, как сравнивать его с конкретными (заранее известными) классами. Поскольку ваш исходный объект и отскочивший объект имеют один и тот же полиморфный базовый тип, следовательно, базовый должен знать все задействованные типы. Эта конструкция серьезно ограничивает расширяемость.
Если вы хотите иметь возможность добавлять любой производный тип, не зная его заранее в базовом классе, тогда вам нужно пройти через динамические типы (будь то dynamic_cast или typeid):
Я предлагаю вам предложение о динамичной расширяемости. Он использует одну диспетчеризацию для сравнения двух объектов одного типа и карту двойной диспетчеризации для сравнения различных типов между ними (по умолчанию возвращает false, если ничего не было объявлено):
struct Base {
typedef bool(*fcmp)(const Base*, const Base*); // comparison function
static unordered_map < type_index, unordered_map < type_index, fcmp>> tcmp; // double dispatch map
virtual bool operator==(const Base& rhs) const
{
if (typeid(*this) == typeid(rhs)) { // if same type,
return equalityStrict(&rhs); // use a signle dispatch
}
else { // else use dispatch map.
auto i = tcmp.find(typeid(*this));
if (i == tcmp.end() )
return false; // if nothing specific was foreseen...
else {
auto j = i->second.find(typeid(rhs));
return j == i->second.end() ? false : (j->second)(this, &rhs);
}
}
}
virtual bool equalityStrict(const Base* rhs) const = 0; // for comparing two objects of the same type
};
Класс A будет тогда переписан как:
struct A : public Base {
A(int eh) : a(eh) {}
int a;
bool equalityStrict(const Base* rhs) const override { // how to compare for the same type
return (a == dynamic_cast<const A*>(rhs)->a);
}
};
С помощью этого кода вы можете сравнивать любые объекты с объектами того же типа. Теперь, чтобы показать расширяемость, я создал struct X
с теми же членами, что и A
, Если я хочу разрешить копировать A с X, я просто должен определить функцию сравнения:
bool iseq_X_A(const Base*x, const Base*a) {
return (dynamic_cast<const X*>(x)->a == dynamic_cast<const A*>(a)->a);
} // not a member function, but a friend.
Затем, чтобы заставить работать динамический двойной диппатч, я должен добавить эту функцию на карту двойной отправки:
Base::tcmp[typeid(X)][typeid(A)] = iseq_X_A;
Тогда результаты легко проверить:
Base *w = new A(1), *x = new A(2), *y = new X(2);
std::cout << (*w == *w) << "\n"; // true returned by A::equalityStrict
std::cout << (*w == *x) << "\n"; // false returned by A::equalityStrict
std::cout << (*y == *x) << "\n"; // true returned by isseq_X_A
Если вам нужен оператор виртуального равенства и вы хотите избежать динамического вещания, вы можете добавить тип enum в базовый класс, который указывает тип, и создать виртуальный получатель, а затем выполнить статическое приведение, если оно совпадает.
enum ClassType { AType, BType };
Class A
{
public:
virtual ClassType getType ();
virtual bool operator ==(const A& a ) const;
};
// in B implementation
virtual bool B::operator ==(const A& a)
{
if (!A::operator==(a)) return false;
if(a.getType () != ClassType::BType) return false;
const B& b = static_cast <const& B>(a);
// do B == checks
return true;
}
Хотя медленно из-за dynamic_cast
Я думаю, что наиболее удобным решением является следующее:
#include <iostream>
struct Base {
virtual bool operator==(const Base& rhs) const
{ return equalityBounce(&rhs); }
virtual bool equalityBounce(const Base* lhs) const = 0;
};
template<typename Derived>
struct BaseHelper : public Base
{
bool equalityBounce(const Base* rhs) const
{
const Derived* p_rhs = dynamic_cast<const Derived*>(rhs);
if (p_rhs == nullptr)
return false;
else
return p_rhs->equalityCheck
(reinterpeter_cast<const Derived*>(this));
}
virtual bool equalityCheck(const Derived*) const = 0;
};
struct A : public BaseHelper<A> {
A(int eh) : a(eh) {}
int a;
virtual bool equalityCheck(const A* rhs) const
{ return a == rhs->a; }
};
int main() {
Base *w = new A(1), *x = new A(2);
std::cout << (*w == *w) << "\n"; // Prints 1.
std::cout << (*w == *x) << "\n"; // Prints 0.
}
Таким образом, вам нужно только заботиться о сравнении объектов того же производного типа, что и "я". Вам не нужно писать больше перегрузок, потому что BaseHelper делает всю вспомогательную работу за вас.