I am developing a tool that will work with MPI, this tool will use multiple functions and each function might be totally different to each other, like signature and number of parameters. C++ version does not need a minimum version, I am assuming this will be compiled with the most recent one.
The idea is that I will push some arguments and a function ID, and eventually serialize them to be passed with MPI. It will be received by another process and with some deserializer it will build a tuple or parameter pack that will be passed to the function #ID.
I want to create a vector of functions such that with this #ID, which is essentially the index in the vector, I could chose the corresponding function and pass the aformentioned tuple.
My tool will receive this vector of functions, then their types will be known at compile time, tuple types would be known at compile time as well, but the function and the tuple to be used will depend on the #ID received.
I have tried std::variant like this, but I don't know how to make it work since C++ needs to know data types at compile time.
#include <iostream>
#include <functional>
#include <variant>
#include <vector>
int foo(int a) {
return a;
}
float bar(int a, float b) {
return (float) a * b;
}
float zor(float a, int b, float c) {
return a * (float) b * c;
}
template<typename F, typename Tuple, size_t... I>
static auto unpack_tuple(F &&f, Tuple &t, std::index_sequence<I...>) {
//I might have to ignore or to add some arguments in here,
//I know the existence of std::apply
return f(std::get<I>(t)...);
}
template<typename F, typename Tuple>
static auto unpack_tuple(F &&f, Tuple &t) {
static constexpr auto size = std::tuple_size<Tuple>::value;
return unpack_tuple(f, t, std::make_index_sequence<size>{});
}
template<typename Functions, typename Tuples>
void magicTool(Functions &functions, Tuples &tuples) {
// receivebuffer using MPI and function id
// use some deserializer and build arguments tuple, using Id to retrieve data types from tuples[id]
//idea
unpack_tuple(functions[id],tuple); //certainly error in this line
}
int main() {
typedef std::function<int(int)> Type1;
typedef std::function<float(int, float)> Type2;
typedef std::function<float(float, int, float)> Type3;
std::vector<std::variant<Type1, Type2, Type3>> functions;
typedef std::tuple<int> Tup1;
typedef std::tuple<int, float> Tup2;
typedef std::tuple<float, int, float> Tup3;
std::vector<std::variant<Tup1, Tup2, Tup3 >> tuples; //this could be also changed by tuple of tuples
functions.emplace_back(foo);
functions.emplace_back(bar);
functions.emplace_back(zor);
// initially just to let the compiler know their type, values won't
// be necessarly used
int a = 3;
float b = 6.4534;
auto t1 = std::make_tuple(a);
auto t2 = std::make_tuple(a, b);
auto t3 = std::make_tuple(b, a, b);
tuples.emplace_back(t1);
tuples.emplace_back(t2);
tuples.emplace_back(t3);
magicTool(functions,tuples);
return 0;
}
Considering that to expand a tuple, it is usually done by creating a recursive helper and using index sequencing, like this
template<typename F, typename Tuple, size_t... I>
static auto unpack_tuple(F &&f, Tuple &t, std::index_sequence<I...>) {
return f(std::get<I>(t)...);
}
template<typename F, typename Tuple>
static auto unpack_tuple(F &&f, Tuple &t) {
static constexpr auto size = std::tuple_size<Tuple>::value;
return unpack_tuple(f, t, std::make_index_sequence<size>{});
}
But in this case I will need to to the same the the variants and tuples at the same same time so at the end I have something like this
Callable(args...); //Callable must match with parameter pack
I tried several options but I always stumble with the fact that I need to know beforehand the data types and a condition cannot be used because compiler would be able to match one function and throw error with the others.
How do I do that?
Please find below a python snip of what I want to do in C++.
import random
def foo(a: int): # function 0
return a
def bar(a: int, b: float): # function 1
return a*b
def zor(a: float, b: int, c: float): # function 2
return a*c+b
def forward(f, *args): # tuple forwarder to any function
return f(*args)
def magicTool(Callables):
packSize: int = random.randrange(3) # to emulate what function I will need
fId = packSize
print("random : {}".format(packSize))
argsPack = [] # to emulate the dynamic size of the incoming arguments
for i in range(packSize+1): # this only matches the parameter with the random function
arg = random.uniform(1.1, 10)
argsPack.append(arg)
print(forward(Callables[fId], *argsPack)) # this tries the function
myCallable = [foo, bar, zor]
magicTool(myCallable)
You can use std::apply
to pass a tuple on as the arguments to a function.
For storing the functions, you need some kind of type erasure. I opted for std::any
in this example.
To store the functions with an ID, i used a std::map.
#include <iostream>
#include <functional>
#include <any>
#include <map>
#include <tuple>
int foo(int val) {
return val;
}
float bar(float val1, int val2) {
return val1 * val2;
}
void zor(int i) {
std::cout << i << '\n';
}
struct FuncCollection {
std::map<int, std::function<std::any(std::any)>> funcMap;
template <typename Ret, typename... Args>
void addFunc(int id, Ret (*fPtr)(Args...)) {
funcMap[id] = [=](std::any args) -> std::any {
auto tuple = std::any_cast<std::tuple<Args...>>(args);
if constexpr(std::is_same_v<Ret, void>) {
std::apply(fPtr, tuple);
return 0;
} else {
return std::apply(fPtr, tuple);
}
};
}
template <typename... Args>
auto callFunc(int id, std::tuple<Args...> args) {
return funcMap[id](args);
}
};
int main()
{
FuncCollection fc;
fc.addFunc(1, foo);
fc.addFunc(2, bar);
fc.addFunc(3, zor);
std::tuple<int> p1{1};
std::tuple<float, int> p2{3.14, 2};
std::tuple<int> p3{5};
auto r1 = fc.callFunc(1, p1);
auto r2 = fc.callFunc(2, p2);
fc.callFunc(3, p3);
std::cout << std::any_cast<int>(r1) << ' ' << std::any_cast<float>(r2) << '\n';
}
This is just an example, and it especially lacks sufficient error checks. std::any_cast
will throw an exception on an invalid cast.