Реализация композиционного поведения для виртуальных методов - PullRequest
1 голос
/ 09 июля 2019

Предположим, что у меня есть иерархия нескольких классов:

class A {
public:
    virtual void DoStuff() = 0;
};

class B : public A {
public:
    // Does some work
    void DoStuff() override;
};

class C : public B {
public:
   // Calls B::DoStuff and does other work
   void DoStuff() override;
};

Это может быть наивно реализовано:

void Derived::DoStuff() {
    Base::DoStuff();
    ...
}

Я полагаю, что эта реализация имеет серьезную проблему: всегда естьНе забывайте вызывать базовую реализацию при переопределениях.

Альтернатива:

class A {
public:
    void DoStuff() {
        for (auto& func: callbacks_) {
            func(this);
        }
    }

    virtual ~A() = default;
protected:
    template <class T>
    void AddDoStuff(T&& func) {
        callbacks_.emplace_back(std::forward<T>(func));
    }

private:
    template <class... Args>
    using CallbackHolder = std::vector<std::function<void(Args...)>>;

    CallbackHolder<A*> callbacks_;
};

Использование:

class Derived : public Base {
public:
    Derived() {
        AddDoStuff([](A* this_ptr){
            static_cast<Derived*>(this_ptr)->DoStuffImpl();
        });
    }
private:
    void DoStuffImpl();
};

Однако я считаю, что при использовании этого метода возникают значительные накладные расходы.фактически вызывает DoStuff(), по сравнению с первой реализацией.В тех случаях использования, которые я видел, возможно, долгое создание объектов не является проблемой (можно также попытаться реализовать что-то вроде «короткой векторной оптимизации», если он этого хочет).

Кроме того, я считаю, что 3 определения длякаждый DoStuff метод - это слишком много.

Я знаю, что его можно очень эффективно решить, используя шаблон наследования, симулирующий CRTP, и можно скрыть решение на основе шаблона за интерфейсным классом (A в примере), но я продолжаю задаваться вопросом - разве не должно быть более простого решения?

Меня интересует хорошая реализация call DERIVED implementation FROM BASE, if and only if derived class exists and it has an overriding method для длинных цепочек наследования (или что-то эквивалентное).

Спасибо!

Редактировать: Мне известна идея, описанная в ответе @ Jarod42, и я не нахожу ее подходящей, потому что считаю, что она уродлива для длинных цепочек наследования - каждый имеетиспользовать разные имена методов для каждого уровня иерархии.

Ответы [ 2 ]

2 голосов
/ 09 июля 2019

Я не уверен, правильно ли я понял, но, похоже, это очень хорошо решается с помощью "Сделать публичный интерфейс не виртуальным, вместо этого виртуализировать частные функции" advice.

Я думаюон обозначен по принципу Open-Closed.Техника заключается в следующем:

#include <iostream>
class B {
    public:
    void f() {
        before_f();
        f_();
    };

    private:
    void before_f() {
        std::cout << "will always be before f";
    }

    virtual void f_() = 0;
};

class D : public B{
    private:
    void f_() override {
        std::cout << "derived stuff\n";
    }
};

int main() {
    D d;
    d.f();
    return 0;
}

Вы существенно лишаете наследующий класс переопределяющего открытого интерфейса, настраивая только открытые части.Базовый класс B строго следит за тем, чтобы требуемый метод вызывался до того, как может потребоваться фактическая реализация в производном.В качестве бонуса вам не нужно забывать звонить в базовый класс.

Конечно, вы также можете сделать f виртуальным и позволить D решать.

2 голосов
/ 09 июля 2019

Вы можете изменить свой класс B на что-то вроде:

class A {
public:
    virtual ~A() = default;
    virtual void DoStuff() = 0;
};

class B : public A {
public:
    void DoStuff() final { /*..*/ DoExtraStuff();  }

    virtual void DoExtraStuff() {}
};

class C : public B {
public:
   void DoExtraStuff() override;
};
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...