Problem: I get a command ID(int) and a data buffer (bytes), and I want to invoke a function based on this ID. Each ID has an associated user-defined data type(known at compile time) and the incoming data bytes conform to this data type. The function also needs to work with a known internal data pointer that is initialized when the program starts (in the constructor perhaps)
Idea: I intend to make a function dispatcher that can take different types of arguments. Given having multiple function signatures mapped in a container is not feasible, I tried to make my function dispatcher a map of std::functions that take BaseArgs*
as a common pointer.
Each of my function callbacks now internally casts to different DerivedArgs
structs.
Each DerivedArg
consists of a pointer to a member of GiantFoo
(also known at compile time) and an incoming data buffer that can be one of DataFooN
. For example, DerivedArgs0
points to foo0
member of GiantFoo
and a pointer to DataFoo0
.
Question: In main, I only know the id
of the function pointer to invoke it. I still need to cast the incoming data_buffer to an appropriate DataFoo1
type before invoking the function.
Do I need another map of IDs to instantiated DerivedArgs (not sure if I can make such a container given they're different types)? Or is there a better way?
#include <functional>
#include <unordered_map>
#include <cstdint>
struct MinorFoo0
{
int a;
};
struct MinorFoo1
{
int a;
};
struct GiantFoo
{
MinorFoo0 foo0;
MinorFoo1 foo1;
};
GiantFoo giant_foo;
struct DataFoo0
{
int x;
};
struct DataFoo1
{
int x;
};
struct BaseArgs
{
uintptr_t giantfoo_ptr;
virtual ~BaseArgs() = default;
};
struct DerivedArgs0 : BaseArgs
{
DataFoo0* foo0;
DerivedArgs0() {
this->giantfoo_ptr = reinterpret_cast<uintptr_t>(&giant_foo.foo0);
}
};
struct DerivedArgs1 : BaseArgs
{
DataFoo1* foo1;
DerivedArgs1() {
this->giantfoo_ptr = reinterpret_cast<uintptr_t>(&giant_foo.foo1);
}
};
using MyFnPtr = std::function<int(BaseArgs*)>;
int foo0_cb(BaseArgs* args)
{
auto foo0_arg = dynamic_cast<DerivedArgs0*>(args);
return 0;
}
int foo1_cb(BaseArgs*)
{
return 0;
}
enum MyFnEnums
{
FOO_0 = 0,
FOO_1 = 1
};
std::unordered_map<int, MyFnPtr> mymap {
{FOO_0, foo0_cb},
{FOO_1, foo1_cb}
};
int main()
{
uint8_t data_buffer[100] = {};
int id = 0;
void* data = &data_buffer;
// How to fit data to a DerivedArgsN and pass to mymap?
return 0;
}
This sounds like the server side of RPC. And while there's a myriad of solutions, the basic problem boils down to deciding what to call, and then transforming the arguments to the appropriate call. And you know what all of them are at compile-time. So let's treat it that way
using DispatchFunction = std::function<int(uint8_t*, size_t)>;
const uint32_t FUNC_ID1 = 1;
int actualWork1(const Type1& argument) {
// Do stuff!
return 0;
}
const uint32_t FUNC_ID2 = 2;
int actualWork2(const uint32_t arg1, const double arg2) {
// Do other stuff!
return -1; // Return error!
}
void doVariousThings(){
// Set up our map of stuff
std::unordered_map<uint32_t, DispatchFunction> dispatcher = {
{ FUNC_ID1, [](uint8_t* data_ptr, size_t data_size) -> int {
// Deserialize the data to the needed type
Type1 currentArg = decodeFor_actualWork1(data_ptr, data_size); // Or make this a constructor of Type1
return actualWork1(currentArg);
};
}, // End of first map entry
{ FUNC_ID2, [](uint8_t* data_ptr, size_t data_size) -> int {
// Deserialize the data to the needed TWO types
uint32_t firstArg{};
double secondArg{};
bool failureToTranslate = decodeFor_actualWork2(data_ptr, data_size, firstArg, secondArg); // Assume signature is (uint8_t*, size_t, uint32_t&, double&)
// Check for failure to translate here, but for the example, just assume it worked
return actualWork2(firstArg, secondArg);
};
}, // End of second map entry
};
// Now just take the ID from the stream and call the functional
vector<uint8_t> buffer = stream.recv() // get the data, however you get it
uint32_t ID = *((uint32_t*)buffer.data());
// Call the needed function
uint8_t* data_ptr = buffer.data() + sizeof(uint32_t); // past the ID header
size_t data_size = buffer.size() - sizeof(uint32_t); // size without header
int result_of_function_call = dispatcher[ID](data_ptr, data_size);
return;
}
There's a couple more things you could wrap up there to be cleaner (type translation should account for failures for example, probably an enum
or enum class
for the IDs), but this is the general idea. Your common format is your actual datastream. Extract the ID of what you're calling, and pass the data into the DispatchFunction
to translate back into your arguments and call your actual destination function. If you need to capture something from the context (like if all of this is in a class, capturing this
), capture it into the lambdas there. Then you can even call member functions no problem.
You don't even need any classes, just std::function
and you're done. You can be more clever, and use std::bind
if you want to, but I think this demonstrates the basics with no extras.
Classes are great, and sublclassing is very useful, but you don't need it for this, and I don't think it adds clarity here.