Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Varadic template to tuple is reversed

Tags:

I have the following code which reads values/parameters from a pointer and calls a function:

#include <iostream>
#include <functional>
#include <string>
#include <tuple>

template<typename T>
T Read(void*& ptr)
{
    T result = *static_cast<T*>(ptr);
    ptr = static_cast<T*>(ptr) + 1;
    return result;
}

template<typename T>
void Write(void*& ptr, const T &value)
{
    *static_cast<T*>(ptr) = value;
    ptr = static_cast<T*>(ptr) + 1;
}

template<typename R, typename... Args>
R Call(void* arguments, R (*func)(Args...))
{
    //args = [c, b, a] somehow..?
    auto args = std::make_tuple(Read<std::decay_t<Args>>(arguments)...);
    return std::apply(func, args);
}


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


int main()
{
    int a = 1024;
    int b = 2048;
    int c = 3072;

    int* args = new int[3];
    void* temp = args;
    
    Write(temp, a);
    Write(temp, b);
    Write(temp, c);
    
    Call(args, func_one);
    delete[] args;
    
    return 0;
}

However if I do:

auto args = std::tuple<Args...>();
std::apply([&arguments](auto&... args) {((args = Read<std::decay_t<decltype(args)>>(arguments)), ...);}, args);

Then args = [a, b, c]. It's in the correct order. In the latter code, I used the comma operator to read each value and assigned it to the tuple.

I have tried: std::invoke(func, Read<std::decay_t<Args>>(arguments)...); Which results in the order of arguments being in reverse as well.

So.. What is the difference between the two below:

Read<Args>(arguments)...  //tuple is reversed - a: 3072 b: 2048 c: 1024
//and
(Read<Args>(arguments)), ...)  //tuple is in the right order - a: 1024 b: 2048 c: 3072

And why does the first one create a tuple in reversed order? Is there a better way to do it that using std::apply to get the correct order?

like image 897
Brandon Avatar asked Dec 12 '20 05:12

Brandon


1 Answers

The arguments in a function call expression are indeterminately sequenced ([expr.call]/8). That is, in the call to std::make_tuple(Read<int>(arguments), Read<int>(arguments), Read<int>(arguments)) in your code, the Read<int>(arguments) calls happen in whatever order the implementation wants to do them, not in the order they're written. It's not undefined behavior, since there are sequence points between the calls and thus you're not trying to modify arguments multiple times "at once". It's just that the program is defined by the standard to have six possible behaviors (the different orders of executing the calls), and it does not specify which one happens (or even that it is consistent between compilations/runs/calls under the same implementation!). Note that the fact that there's a parameter pack involved changes nothing: the pack expansion is handled by just "plugging in" the expanded syntax in place of the expansion, and then handling that according to non-pack rules ([temp.variadic]/8).

When you use the "fixed" version, the code expands to basically contain

arg1 = Read<int>(arguments), arg2 = Read<int>(arguments), arg3 = Read<int>(arguments);

where the ,s are now the comma operator and not part of the function call syntax. The operands to the comma operator are determinately sequenced, from left to right.

Honestly, I don't know of any truly simpler way to get the right order than what you did. Actually, I think we have the unfortunate situation that if the argument expressions to a function call have side-effects that must be sequenced in a determined order, then there's no way to initialize the function parameters directly from the argument expressions (without a copy or move). Of course this isn't a problem here. Still, your "fixed" version confers unnecessary requirements that the types involved also be default-constructible and assignable, which is not necessary. Note that, for whatever godforsaken reason, if you use braces (list-initialization) to call the constructor of a class (in our case, std::tuple), then the argument expression evaluations will be sequenced left-to-right, guaranteed ([dcl.init.list]/4).

template<typename R, typename... Args>
R Call(void *arguments, R (*func)(Args...)) {
    std::tuple<std::decay_t<Args>...> args{Read<std::decay_t<Args>>(arguments)...};
    return std::apply(func, std::move(args));
}

I know no analogue for normal function calls, so we still incur moves, which is sad (though I hope you aren't writing types with expensive moves!).

Also, I'm sure you already know this, but please note that this entire business with the void* and the casting and all that seems horribly unsafe. In particular, even assuming the callers of all these functions are all doing it "right", the functions themselves are not handling alignment correctly (or at all...).

like image 181
HTNW Avatar answered Sep 30 '22 19:09

HTNW