vendredi 3 juin 2016

Per-method settings in C++ class

I am developing a C++ framework, one part of which is wrapping a low-level C API. This API allows to register callbacks for different "services", each callback have its own signature. Each callback also has a priority.

My task is to wrap this low level API to something convenient and usable by junior developers, so it has to be extremely easy in use and difficult to make a mistake.

The API is something like this:

int on_msg(int size, void* data) {
    MsgData* msg = (MsgData*)data;
    // processing of msg->header, msg->body, etc.
}

register_service(ON_MSG, HIGH_PRIORITY, on_msg);

Services can be also unregistered, but in real problem applications this is never required.

There are a lot of possible ways of wrapping this interface in a framework, but none of my ideas looks perfect. I provide my ideas below and I just want to see if someone can come up with something more elegant and convenient.

In a Framework I'm developing there are a class Application with a bunch of virtual methods intended to be overridden by a user. The current approach is to allow him to override all of the methods like on_msg above. I also provide set_priority method, which has to be called before services are registered. In pseudocode it looks like this:

class Application {
protected:

    Application() {
        initialize();
        register_service(ON_MSG, get_priority(ON_MSG), wrap(&Application::on_msg));
        register_service(ON_KEYBOARD, get_priority(ON_KEYBOARD), wrap(&Application::on_keyboard));
        register_service(ON_IDLE, get_priority(ON_IDLE), wrap(on_idle));
        register_service(ON_CONNECTED, get_priority(ON_CONNECTED), wrap(on_connected);
        // ...
    }

    // Callbacks
    virtual void on_msg(const string& header, const string& body) {}
    virtual void on_keyboard(int key) {}
    virtual void on_idle() {}
    virtual void on_connected() {}
    // ...

    virtual void initialize() = 0;

    void set_priority(Service service, Priority priority) {
        // remembers priority in internal map
        // or throws exception if callback is already registered
    }

    Priority get_priority(Service service) {
        // returns priority which was set before
        // or default priority
    }
};


class UserApplication : public Application {
protected:
    void initialize() override {
        set_priority(ON_IDLE, Low);
    }

private:
    void on_idle() override (
        // something to do
    }
};

There are two problems with this approach:

  1. I have to register all of the services, because there's no way to know which of the methods were overridden by a user. It is wasteful, although from the performance point of view not critical.
  2. User allowed to set priorities only inside intialize() method, but this is not obvious from the interface. Until user's application terminated by unhandled exception, she wouldn't even suspect that her calls to set_priority() outside the initialized() doesn't work.

These two problems could be solved if I provided callback setter methods instead of overriding:

// In class Application:
using OnMsgHandler = std::function<void(const std::string&, const std::string&)>;
set_on_msg(Priority, const OnMsgHandler&);

// In client's code:
set_on_msg(HighPriority,
    [this](const auto& header, const auto& body) {
         on_msg(header, body);
    });

This approach can work, but client's code now looks more complicated. Overriding base class methods seemed easier, signatures were more obvious, there were no need for lambdas/bindings/anything like that. Of course that's not a problem for experienced C++ developers, but the Framework has to be used by very inexperienced guys, so I'd avoid forcing clients to use any "advanced" features. If I wouldn't see better solution, I'll use this approach, but I have feelign that this can be done better.

There's still another option: use the first approach, but allow user to change priority at any time. Unfortunately, the low-level API doesn't have any method for changing priority, so I have to unsubscribe and then subscribe again. Race condition is not a problem here, since I have a way to control it in this API, but subscription can theoretically fail and return some error code. So the following scenario is possible:

  1. User is in a subscribed state with low priority.
  2. User calls set_priority(ON_MSG, HIGH, err);
  3. err now contains error "Cannot subscribe because of some system failure". User remains in unsubscribed state.

This scenario is rare (in fact I'm not sure if register_service can really fail that way - I've never seen it in practice, although API allows it to behave like that), but I still want to avoid id.

Ideally, I'd like to allow a user to override methods somehow like that:

class UserApplication : public Application {

    virtual void on_keyboard(int key, Priority::High) {
        // Priority::High is an empty type here
    }

    // or:

    template<>
    virtual void on_keyboard<Priority::High>(int key)
    { /* ... */ }

    // or:

    [[priority(High)]]
    virtual void on_keyboard(int key)
    { /* ... */ }
};

That way I could know at compile time the required priority and disallow anybody to change it. Unfortunately, I have no idea how anything like that could be implemented in C++ without relying on ugly macroses and I suspect that it maybe impossible to do at all.

So, my question is how is it can be done the best way? Is some of my approaches can be done better or maybe there are some standard solution how interface has to be designed for such problem?

Thanks!

Aucun commentaire:

Enregistrer un commentaire