Home > Blockchain >  Can I allocate a series of variables on the stack based on template arguments?
Can I allocate a series of variables on the stack based on template arguments?

Time:01-09

In a piece of code I'm writing, I receive packets as uint8_t * and std::size_t combination. I can register functions to call with these two parameters, based on which file descriptor the packet was received from. I use an std::map<int, std::function<void(const uint8_t *, std::size_t)> > handlers to keep track of which function to call.

I would like to be able to (indirectly) register functions with arbitrary arguments. I already have a function like this to transform from the uint8_t * and std::size_t to separate variables:

int unpack(const uint8_t *buf, std::size_t len) { return 0; }

template <typename T, typename... Types>
int unpack(const uint8_t *buf, std::size_t len, T &var1, Types... var2) {
  static_assert(std::is_trivially_copyable<T>::value, "unpack() only works for primitive types");
  if (len < sizeof(T)) return -1;
  var1 = *reinterpret_cast<const T *>(buf);
  const auto sum = unpack(buf   sizeof(T), len - sizeof(T), var2...);
  const auto ret = (sum == -1) ? -1 : sum   sizeof(T);
  return ret;
}

My question is: Is it possible with C 20 to auto-generate a function that convers from uint8_t * and std::size_t to the arguments that a passed function needs?

I would like to be able to do this:

void handler(unsigned int i) { ... }

int main(int argc, char ** argv) {
/* some code generating an fd */
handlers[fd] = function_returning_an_unpacker_function_that_calls_handler(handler);

edit: I realize I went a bit too short on my answer, as some mentioned (thanks!).

I am wondering if it is possible (and if so, how?) to implement the function_returning_an_unpacker_function_that_calls_handler function. I started out doing something like this (written from memory):

template<typename... Types>
std::function<void(const uint8_t * buf, std::size_t)>
function_returning_an_unpacker_function_that_calls_handler(std::function<void(Types...)> function_to_call) {
  const auto ret = new auto([fun](const uint8_t * buf, std::size_t len) -> void {
    const auto unpack_result = unpack(buf, len, list_of_variables_based_on_template_params);
    if(unpack_result == -1) return nullptr;
    function_to_call(list_of_variables_based_on_template_params);
  };
  return ret;
}

This is also why I supplied the unpack function. The problem I'm encountering is that I'm struggling with the list_of_variables_based_on_template_params bit. I haven't found any way to generate a list of variables that I can repeat identically in two places. I also looked a little bit into using std::tuple::tie and friends, but I didn't see a solution there either.

CodePudding user response:

It's possible, just annoying to write.

First you need a trait to get parameters from a function type:

template <typename T>
struct FuncTraits {};

#define GEN_FUNC_TRAITS_A(c, v, ref, noex) \
    template <typename R, typename ...P> \
    struct FuncTraits<R(P...) c v ref noex> \
    { \
        template <template <typename...> typename T> \
        using ApplyParams = T<P...>; \
    };

#define GEN_FUNC_TRAITS_B(c, v, ref) \
    GEN_FUNC_TRAITS_A(c, v, ref,) \
    GEN_FUNC_TRAITS_A(c, v, ref, noexcept)

#define GEN_FUNC_TRAITS_C(c, v) \
    GEN_FUNC_TRAITS_B(c, v,) \
    GEN_FUNC_TRAITS_B(c, v, &) \
    GEN_FUNC_TRAITS_B(c, v, &&)

#define GEN_FUNC_TRAITS(c) \
    GEN_FUNC_TRAITS_C(c,) \
    GEN_FUNC_TRAITS_C(c, volatile)

GEN_FUNC_TRAITS()
GEN_FUNC_TRAITS(const)

Then some templates to analyze what kind of callable (function, function pointer, or a functor) you got, and apply the trait accordingly:

template <typename T> struct RemoveMemPtr {using type = T;};
template <typename T, typename C> struct RemoveMemPtr<T C::*> {using type = T;};

template <typename T>
struct ToFuncType {};

template <typename T>
requires std::is_function_v<std::remove_pointer_t<T>>
struct ToFuncType<T> {using type = std::remove_pointer_t<T>;};

template <typename T>
requires requires {&T::operator();}
struct ToFuncType<T>
{
    using type = typename RemoveMemPtr<decltype(&T::operator())>::type;
};

Then you can make a templated functor that automatically unwraps the arguments. Since Unwrap() must be called in order, and function arguments are evaluated in unspecified order, we need a tuple (or something similar) that accepts a braced list:

template <typename T>
T Unpack(char *&, std::size_t &)
{
    std::cout << __PRETTY_FUNCTION__ << '\n';
    return {};
}

template <typename F>
struct WrapFunctor
{
    template <typename ...P>
    struct Impl
    {
        std::decay_t<F> func;
        
        void operator()(char *p, std::size_t n)
        {
            std::apply(func, std::tuple{Unpack<P>(p, n)...});
        }
    };
};

template <typename F>
auto Wrap(F &&func)
{
    using Functor = typename FuncTraits<typename ToFuncType<std::remove_cvref_t<F>>::type>::template ApplyParams<WrapFunctor<F>::template Impl>;
    return Functor{std::forward<F>(func)};
}

Finally, some tests:

void foo(int, float, char)
{
    std::cout << __PRETTY_FUNCTION__ << '\n';
}

int main()
{
    Wrap(foo)(nullptr, 42);
    Wrap(&foo)(nullptr, 42);
    Wrap([](int, float, char){std::cout << __PRETTY_FUNCTION__ << '\n';})(nullptr, 42);
}

I've changed the signature of Unpack() to take the parameters by reference and unpack one variable at a time.


Almost forgot: var1 = *reinterpret_cast<const T *>(buf); is a strict aliasing violation and UB. Prefer memcpy.

CodePudding user response:

This answer is very similar to the first one, but it leverages the use of CTAD and std::function to figure out the function signature.

Creates a tuple based on the function signature, and passes both the argument types and the elements from the tuple on to unpack.

#include <iostream>
#include <tuple>
#include <type_traits>
#include <cstring>
#include <functional>

int unpack(const uint8_t *buf, std::size_t len) { return 0; }

template <typename T, typename... Types>
int unpack(const uint8_t *buf, std::size_t len, T &var1, Types&... var2) {
  static_assert(std::is_trivially_copyable<T>::value, "unpack() only works for primitive types");
  if (len < sizeof(T)) return -1;
  var1 = *reinterpret_cast<const T *>(buf);
  std::cout << "In unpack " << var1 << "\n";
  const auto sum = unpack(buf   sizeof(T), len - sizeof(T), var2...);
  const auto ret = (sum == -1) ? -1 : sum   sizeof(T);
  return ret;
}

template<typename T, typename R, typename... Args>
std::function<void(const uint8_t * buf, std::size_t)>
unpack_wrapper_impl(T function_to_call, std::function<R(Args...)>) {
    return [function_to_call](const uint8_t *buf, std::size_t len) -> void {
        std::tuple<std::decay_t<Args>...> tup;
        std::apply([&](auto&... args) {
            unpack(buf, len, args...);
        }, tup);
        std::apply(function_to_call, tup);
    };
}

template<typename T>
std::function<void(const uint8_t * buf, std::size_t)>
unpack_wrapper(T&& function_to_call) {
    return unpack_wrapper_impl(std::forward<T>(function_to_call), std::function{function_to_call});
}

void test(int a, int b) {
    std::cout << a << " " << b << "\n";
}

int main() {
    int a= 5, b = 9;
    uint8_t* buf = new uint8_t[8];
    std::memcpy(buf, &a, 4);
    std::memcpy(buf   4, &b, 4);

    auto f = unpack_wrapper(test);

    f(buf, 8);
}
  •  Tags:  
  • Related