Как я могу реализовать двойную диспетчеризацию, если я не знаю все классы заранее?

У меня есть базовый класс с (потенциально) большим количеством подклассов, и я хотел бы иметь возможность сравнивать любые два объекта базового класса на равенство. Я пытаюсь сделать это, не вызывая кощунственное ключевое слово 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 делает всю вспомогательную работу за вас.

Другие вопросы по тегам