Home > front end >  Generic callback registration using templates
Generic callback registration using templates

Time:08-26

I'm currently developing a http based server component which allows to handle incoming requests. Each incoming request contains a specific path which can be registered to a corresponding callback/std::function to implement the servers business logic behind this callback.

The aim is to de/serialize the body of each incoming request. The request body is a class template to access the including body in a type-safe way. The body could be for example either a std::string or some other type like a Protobuf message. I define the type as a template argument to pass it to the std::function.

I have created the following architecture to provide an API to register different types to a std::function in a generic way. My first idea was using templates to realize this.

// The Request class without any details, e.g. the path member to simplify
class Request{};

template <typename T>
class Body : public Request {
public:
    T getValue() { return m_value; }
private:
    T m_value;
};

To handle different types, there is a strategy pattern implemented representing those certain serializable types.

// serializable interface
class ISerializable
{
public:
    virtual std::vector<uint8_t> serialize() = 0;
    virtual void deserialize(std::vector<uint8_t>) = 0;
};

// class template for concrete implementation of serializable types
template <typename ValueType>
class SerA : public ISerializable
{
public:
    std::vector<uint8_t> serialize() override
    {
        std::cout << "SerA::serialize() called";
        return {};
    }
    void deserialize(std::vector<uint8_t>) override
    {
        std::cout << "SerA::deserialize() called";
    }
private:
    ValueType m_value;
};

template <typename ValueType>
class SerB : public ISerializable
{
public:
    std::vector<uint8_t> serialize() override
    {
        std::cout << "SerB::serialize() called";
        return {};
    }
    void deserialize(std::vector<uint8_t>) override
    {
        std::cout << "SerB::deserialize() called";
    }
private:
    ValueType m_value;
};

Next code is a handler class which allows to register a serializable types to a specific std::function. This function shall added to a map in order to execute the matching callback by an incoming request that fits to the registered path.

template <typename TSerializable>
using Callback = std::function<void(Body<TSerializable>& request)>;

class Handler
{
    template <typename TSerializable>
    using CallbackMap = std::unordered_map<std::string, Callback<TSerializable>>;
public:

// use type traits to make sure the incoming type is always derived from ISerializable
    template <typename TSerializable, std::enable_if_t<std::is_base_of_v<ISerializable, TSerializable>, bool> = true>
    void add(std::string topic, Callback<TSerializable> t)
    {
        map_.try_emplace(topic, t);
    }

private:
    CallbackMap<ISerializable*> map_;
};

The usage of this API looks like follows

int main()
{
    Handler handler;
    handler.add<SerA<int>>("topic1", [](Body<SerA<int>>& request)
        {
    // user code implementation here
    // deserialized body shall be accessible as int type here
        });

    handler.add<SerA<std::string>>("topic2", [](Body<SerA<std::string>>& request)
        {
    // user code implementation here
    // deserialized body shall be accessible as std::string type here
        });

// SerB could be a serializable type of a protobuf message or anything else. This is just an example code
    //handler.add<SerB<some::protobuf::Message>>("topic3", [](Body<SerB<some::protobuf::Message>>& request)
    //  {
    // user code implementation here
    // serialized body shall be accessible as concrete protobuf message type here
    //  });
}

Calling the add method will lead to the following compiler error.

Error C2664 'std::function<void (Body<ISerializable *> &)>::function(std::nullptr_t) noexcept': cannot convert argument 1 from 'std::function<void (Body<SerA> &)>' to 'std::nullptr_t'

Obviously I have a problem with the pointer to ISerializable here.

My question is, am I using templates right here or do I misuse it maybe? Is it possible to solve this problem using templates and if yes what am I missing? What is the compiler doing here and why is it not possible to pass Body<SerA> to Body<ISerializable*>? Do I need any wrapper class around my callback to let do the conversion for me?

Maybe it's also possible to use std::variant or variadic templates to solve this problem? But I'm absolutely not familiar with it.

CodePudding user response:

class RequestRaw {
public:
    std::vector<uint8_t> getBody() const {return m_body;}
private:
    std::vector<uint8_t> m_body = {1};
};
 
template <typename ValueType>
class ScalarTypeSerializale {
public:
    void deserialize(std::vector<uint8_t> bytes) {
        m_value = bytes[0];
    }
 
    ValueType getValue(){return m_value;}
private:
    ValueType m_value;
};
 
template <typename ValueType>
class StringTypeSerializale {
public:
    void deserialize(std::vector<uint8_t> bytes) {
        m_value = std::to_string(bytes[0])   "string";
    }
 
    ValueType getValue(){return m_value;}
private:
    ValueType m_value;
};
 
class Request {
public:
    std::string m_path;
};
 
template <typename T>
class Body : public Request {
public:
    void setBody(const T& body) { m_body = body; }
    T getBody() const { return m_body; }
private:
    T m_body;
};
 
class Handler {
public:
    template<typename TSerializer>
    void addHandler(const std::string & path, std::function<void(const Body<TSerializer> &)> callback) {
        map.insert({
            path,
            [callback](const RequestRaw & raw_request) {
                TSerializer serializer;
                serializer.deserialize(raw_request.getBody());
                Body<TSerializer> typedRequest;
                typedRequest.setBody(serializer);
                callback(typedRequest);
            }
        });
    }
    void processRequest(const std::string & path, const RequestRaw & rawRequest) {
        map[path](rawRequest);
    }
private:
    std::unordered_map<std::string, std::function<void(const RequestRaw &)>> map;
};
 
void run_request_handlers() {
    Handler handler;
 
    handler.addHandler<ScalarTypeSerializale<int>>(
        "/my/path",
        [](const Body<ScalarTypeSerializale<int>> & typedRequest) {
            int reveivedRequestBody = typedRequest.getBody().getValue();
            std::cout << "\n int handler \n " << reveivedRequestBody;
        }
    );
 
    handler.addHandler<StringTypeSerializale<std::string>>(
        "/my/path2",
        [](const Body<StringTypeSerializale<std::string>> & typedRequest) {
            std::string reveivedRequestBody = typedRequest.getBody().getValue();
            std::cout << "\n string handler \n " << reveivedRequestBody;
        }
    );
 
    const RequestRaw rawRequest;
 
    handler.processRequest("/my/path", rawRequest);
    handler.processRequest("/my/path2", rawRequest);
}
  • Related