2

Please consider the following sample

#include <cstdio>
#include <functional>
#include <memory>
#include <utility>

class PaintDelegate {
    public:
        virtual ~PaintDelegate() = default;
        virtual void draw(){}
};

class PaintDelegateActions {
    public:
        PaintDelegateActions(std::function<void()>&& drawAction):draw_(std::move(drawAction)) {}
        void draw() {
            draw_();
        }
    private:
        std::function<void()> draw_;
};

class Renderer {
    public:
        Renderer(std::shared_ptr<PaintDelegate> delegate): delegate_(delegate){}
        Renderer(std::unique_ptr<PaintDelegateActions>&& actions): actions_(std::move(actions)){}
        /*  For simplicity just grouped both routes in the same class
            these should be 2 different implementations
        */
        void paint() {
            if(delegate_) {
                delegate_->draw();
            }
            if(actions_) {
                actions_->draw();
            }
        }
    private:
        std::shared_ptr<PaintDelegate> delegate_;
        std::unique_ptr<PaintDelegateActions> actions_;
};

class DriverPaintDelegate;

class RenderDriver: public std::enable_shared_from_this<RenderDriver> {
    public:
        RenderDriver();
        void render();
        void doPaint();
    private:
        std::shared_ptr<DriverPaintDelegate> delegate_;
        std::unique_ptr<Renderer> renderer_;
        std::unique_ptr<Renderer> rendererActions_;
};

class DriverPaintDelegate: public PaintDelegate {
    public:
        void setContext(std::weak_ptr<RenderDriver> context) {
            context_ = context;
        }
        void draw() override {
            if(auto renderer = context_.lock()) {
                std::printf("Trigger paint from delegate\n");
                renderer->doPaint();
            }
        }
    private:
        std::weak_ptr<RenderDriver> context_;
};

RenderDriver::RenderDriver():
    delegate_(std::make_shared<DriverPaintDelegate>()),
    renderer_(std::make_unique<Renderer>(delegate_)) {}

void RenderDriver::render() {
    /*  Can't pass weak ref in the constructor of RenderDriver
        since weak_from_this isn't useful yet
    */   
    delegate_->setContext(weak_from_this());
    rendererActions_ = std::make_unique<Renderer>(
        std::make_unique<PaintDelegateActions>([context = weak_from_this()](){
            if(auto self = context.lock()) {
                std::printf("Trigger paint from actions\n");
                self->doPaint();
            }
        })
    );

    /*  Just to trigger the delegates/actions, in practice these would
        be invoked from within the Renderer class upon some event etc.
    */
    renderer_->paint();
    rendererActions_->paint();
}

void RenderDriver::doPaint() {}

int main() {
    auto driver = std::make_shared<RenderDriver>();
    driver->render();
    return 0;
}

What I intend to demo through this is two ways of implementing delegates, one via an interface (PaintDelegate) and another via actions (PaintDelegateActions). Both achieve the same functionality but in the former route, one needs to

  • Provide a derived implementation of the delegate (DriverPaintDelegate) which is sent to the Renderer class
  • Provide this derived implementation with a context in case the delegate needs to refer to the actual driver class via a weak reference.
  • The driver class needs to store the delegate_.

There are no such considerations to be made for the actions delegate route since the lambda can capture the weak reference to the driver context. But this route has the following considerations:

  • Either the renderer_ needs to be constructed once there's a valid weak reference or the action delegate could be set on the renderer separately after constructing the renderer_.
  • The driver class doesn't need to store the delegate at its own end

From a scalability perspective, with more delegates to be invoked, the interface route (PaintDelegate) can add more virtual functions but the actions route (PaintDelegateActions) needs to add more std::functions which can make the constructor of PaintDelegateActions as well as the site of the construction of PaintDelegateActions object ugly and unwieldy.

I would like to elicit the views of the experts on when should one opt for these routes or if there is an altogether different recommendation for such delegate pattern implementations.

3
  • Why not pass the context as a method argument to PaintDelegate.draw? Or pass a weak reference to the driver itself? May 26, 2023 at 12:15
  • I'm getting a little lost int he code example, because it combines the two strategies. Do you intend to support both PaintDelegate and actions in the same class, or are you trying to decide between the two? May 26, 2023 at 12:19
  • @GregBurghardt I am trying to decide between the two. I don't have any intentions of mixing both.
    – Zoso
    May 26, 2023 at 18:04

1 Answer 1

2

Using a lambda function of type std::function<void()> or an object which is derived from an abstract class with just one virtual function are both semantically equivalent. More general, objects and closures are semantically equivalent, that is a well known fact and true in lots of modern multi paradigm languages which support both, OOP and functional programming. If you search a little in older questions, you find lots of variants of this question asked by people using other languages like Java, C# or Python.

The "OOP" way usually requires a little bit more boilerplate code. On the other hand, it allows to extend the base class / interface by more functions and hence to create a combined abstraction, a.k.a. "strategy pattern". In C++, full functional support (not just "pointers to function", but closures and std::function) was first introduced in C++11, if I remember correctly.

When you can do both, it is often just a matter of taste which one to prefer. I usually use the functional approach when I just need a single function to pass around, with no need to create a special abstraction for it, and refactor to virtual classes in case things get more complex and I see some benefit in having a combined interface.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.