Search code examples
c++lambdacastingvariadic-templatesraw-pointer

Recasting Variadic Arguments from void** Array in C++


I'm working on a project where I need to convert variadic arguments into a void** array, pass them to a lambda function, and then correctly cast them back to their original types for use in a custom function. While I can successfully create the void** array, I'm having trouble correctly expanding and casting the variadic arguments back to their original types inside the lambda function. The custom function is intended to process these arguments, but incorrect values are being produced, indicating an issue with how I'm handling the void** array and the subsequent casts.

It is important to notice that this is meant to be used for any function, not limited to only the shown types, which differs from the other examples I found on stack overflow.

Below is a minimal reproducible example of my approach. It demonstrates:

  • Capturing the variadic arguments and their addresses.
  • Storing the function pointer and addresses of the arguments as void*.
  • Using a lambda to reinterpret and cast these pointers back to their original types.
  • Applying the custom function to these arguments.

I split the expansion of the parameters for debugging purposes by the use of the function helper(...).

Could someone help me identify why the arguments are not correctly cast back to their original types and how to properly handle this conversion?

#include <iostream>
#include <tuple>

// Implementation of the forwarder function
void raw_forwarder(void (*f)(void **), void **args) {
    f(args);
    // some other procedure in here
}

// Base case helper function
template<typename T>
static auto helper(T &arg) {
    std::cout << "helper 2 - val:\t" << arg << "\tat: " << &arg << std::endl;
    return std::make_tuple(arg);
}

// Recursive helper function
template<typename T, typename... Args>
static auto helper(T &arg, Args &... args) {
    std::cout << "helper 1 - val:\t" << arg << "\tat: " << &arg << std::endl;
    return std::tuple_cat(std::make_tuple(arg), helper(args...));
}

template<typename F, typename... Args>
static void forwarder(F &f, Args &... args) {

    // Create an array of void* to hold the addresses of the arguments
    void *argArray[] = {reinterpret_cast<void *>(&f), reinterpret_cast<void *>(&args)...};

    // Call the raw forwarder with the function and arguments
    raw_forwarder([](void **args_raw) {
                      // Retrieve the function pointer
                      F &reinterpreted_func = *reinterpret_cast<F *>(args_raw[0]);

                      // Create a tuple of argument pointers using the helper functions
                      auto arg_tuple = helper(*reinterpret_cast<Args *>(args_raw[1])...);

                      std::cout << std::endl;

                      // Apply the function to the dereferenced arguments
                      std::apply([&reinterpreted_func](auto &&... unpacked_args) { reinterpreted_func(unpacked_args...); }, arg_tuple);
                  },
                  argArray);
}

void custom_function(int val1, float val2, double val3) {
    std::cout << "val1 = " << val1 << std::endl;
    std::cout << "val2 = " << val2 << std::endl;
    std::cout << "val3 = " << val3 << std::endl;
}

int main() {
    int val1 = 7;
    float val2 = 5.5f;
    double val3 = 10.0;

    helper(val1, val2, val3); // this works
    std::cout << std::endl;

    forwarder(custom_function, val1, val2, val3);

    return 0;
}


From the output of the above snipped, (shown below). I noticed that when expanding the pointers, the issue lies in the fact that the same pointer value is passed for all the parameters. This leads to incorrect values being used in the custom function. How can I correctly expand and cast the variadic arguments to avoid this issue?

helper 1 - val: 7       at: 0x4fcd9ff84c
helper 1 - val: 5.5     at: 0x4fcd9ff848
helper 2 - val: 10      at: 0x4fcd9ff840

helper 1 - val: 7       at: 0x4fcd9ff84c
helper 1 - val: 9.80909e-45     at: 0x4fcd9ff84c
helper 2 - val: 3.45846e-323    at: 0x4fcd9ff84c

val1 = 7
val2 = 9.80909e-45
val3 = 3.45846e-323

For the record, when in debug mode, if a look into the args_raw like this below, I see the the cells do point to the actual values.

  *reinterpret_cast<int*>(args_raw[1])
  *reinterpret_cast<float*>(args_raw[2])
  *reinterpret_cast<double*>(args_raw[3])

Solution

  • In this line,

    auto arg_tuple = helper(*reinterpret_cast<Args*>(args_raw[1])...);
    

    the array index should be incremented together with pack expansion.

    The minimal fix might look like this:

    template<typename F, std::size_t... Is, typename... Args>
    void forwarder_impl(F& f, std::index_sequence<Is...>, Args&... args) {
        // ...
        auto arg_tuple = helper(*reinterpret_cast<Args*>(args_raw[Is + 1])...);
        // ...
    }
    
    template<typename F, typename... Args>
    void forwarder(F& f, Args&... args) {
        forwarder_impl(f, std::index_sequence_for<Args...>(), args...);
    }
    

    Here we introduced one level of indirection to inject a pack of indices Is..., which is then expanded together with Args....

    If you get rid of helper, you can simplify this to:

    template<typename F, typename... Args>
    void forwarder(F& f, Args&... args) {
        // ...
        std::size_t i = 1;
        std::tuple arg_tuple{*reinterpret_cast<Args*>(args_raw[i++])...};
        // ...
    }
    

    Here we use the fact this inside a braced list elements are evaluated in the order in which they appear in the list (per [dcl.init.list/4]).