1

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)

Andres
  • 161
  • 1
  • 7
  • Perhaps it will be useful https://stackoverflow.com/a/65507709/13782669 – alex_noname Jan 14 '21 at 15:45
  • `python snip of what I want to do in C++` and the difference is, python has reflection, c++ does not. Please add missing `#include`s and functions `foo`, `bar`, `zor` declarations and a short `int main()` deifniiton to your code snippet to make it easy to work with. – KamilCuk Jan 14 '21 at 15:50
  • thanks for the comment @alex_noname, in this case there is a conditional and I must explicitly write the possible types. What I'm trying to do, "magicTool" won't know the types, and to find out, it will probably need to iterate recursively, and I don't know how to do it correctly. – Andres Jan 14 '21 at 15:50
  • If you really need to store all the functions in one container, you could use something like `std::function` and wrap all your functions in a lambda that will `any_cast` to the right argument tuple and pass it on to your actual function. – super Jan 14 '21 at 16:01
  • Aside: I don't see the point of `forward` here. Isn't `Callables[fId](*argsPack)` sufficient? – Caleth Jan 14 '21 at 16:07
  • @Caleth thanks for the comment, this is just to emulate that I forward() receives a tuple and this one expand it and inside there'll be f(args...). My objective is to do it in C++, python is only a test to confirm that this was possible, and to express my needs. – Andres Jan 14 '21 at 16:12
  • @KamilCuk thanks for the comment, I fixed it – Andres Jan 14 '21 at 16:13
  • [`std::apply`](https://en.cppreference.com/w/cpp/utility/apply) – Caleth Jan 14 '21 at 16:13
  • @super would you please expand your suggestion? thx – Andres Jan 14 '21 at 16:19

1 Answers1

2

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.

super
  • 12,335
  • 2
  • 19
  • 29
  • Having that `return 0` shows that there's a problem with always returning `std::any`. I think that it should be restricted to `void` functions , or return a particular type – Caleth Jan 14 '21 at 16:28
  • @Caleth Yeah, it makes the code a bit less clean and there are indeed other options. I'll leave that part to OP though, this is at least a working approach that's safe. – super Jan 14 '21 at 16:32