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);
}