Home > Back-end >  How to correctly forward and use a nested tuple of constexpr struct with standard tuple operations
How to correctly forward and use a nested tuple of constexpr struct with standard tuple operations

Time:05-28

I want to store passed data via constexpr constructor of a struct, and store the data in a std::tuple, to perform various TMP / compile time operations.

Implementation

template <typename... _Ts>
struct myInitializer {
    std::tuple<_Ts...> init_data;

    constexpr myInitializer(_Ts&&... _Vs) 
        : init_data{ std::tuple(std::forward<_Ts>(_Vs)...) }
    {}
};

Stored data uses a lightweight strong type struct, generated via lvalue and rvalue helper overload:

template <typename T, typename... Ts>
struct data_of_t {
    using type = T;
    using data_t = std::tuple<Ts...>;
    data_t data;

    constexpr data_of_t(Ts&&... _vs)
        : data(std::forward<Ts>(_vs)...)
    {}
};
template<typename T, typename... Ts>
constexpr auto data_of(Ts&&... _vs) {
    return data_of_t<T, Ts...>(std::forward<Ts>(_vs)...);
};

template<typename T, typename... Ts>
constexpr auto data_of(Ts&... _vs) {
    return data_of_t<T, Ts...>(std::forward<Ts>(_vs)...);
};

It's implemented like

template <typename T = int>
class test {
public:
    static constexpr auto func(int p0=0, int p1=1, int p2=3) noexcept {
        return data_of <test<T>>
            (data_of<test<T>>(p0, p1));
    }
};
int main() {
    constexpr // fails to run constexpr // works without
    auto init = myInitializer (
        test<int>::func()
        ,test<int>::func(3)
        ,test<int>::func(4,5)
    );

    std::apply([&](auto&&... args) {
        //std::cout << __PRETTY_FUNCTION__ << std::endl;
        auto merged_tuple = std::tuple_cat(std::forward<decltype(args.data)>(args.data)...);
        }
        , init.init_data);
}

Getting to the point

std::tuple_cat fails if myInitializer instance is constexpr.

std::apply([&](auto&&... args) {
        auto merged_tuple = std::tuple_cat(std::forward<decltype(args.data)>(args.data)...);

It appears to be related to the const qualifier added via constexpr.

How can this be fixed?

See full example at https://godbolt.org/z/j5xdT39aE

CodePudding user response:

This:

auto merged_tuple = std::tuple_cat(std::forward<decltype(args.data)>(args.data)...);

is not the right way to forward data. decltype(args.data) is going to give you the type of that data member - which is not a function of either the const-ness or value category of args. Let's take a simpler example:

void f(auto&& arg) {
    g(std::forward<decltype(arg.data)>(arg.data));
}

struct C { int data; };

C c1{1};
const C c2{2};

f(c1); 
f(c2);
f(C{3});

So here I have three calls to f (which call f<C&>, f<const C&>, and f<C>, respectively). In all three cases, decltype(arg.data) is... just int. That's what the type of C::data is. But that's not how it needs to be forwarded (it won't compile for c2 because we're trying to cast away const-ness -- as in your example -- and it'll erroneously move out of c1).

What you want is to forward arg, separately, and then access data:

void f(auto&& arg) {
    g(std::forward<decltype(arg)>(arg).data);
}

Now, decltype(arg) actually varies from instantiation to instantiation, which is a good indicator that we're doing something sensible.

CodePudding user response:

In addition of the forwarding problem denoted by Barry, there's a different reason why you cannot have constexpr on init. This is because you contain a reference to a temporary inside data_of_t.

You see, you are containing a type obtained from overload resolution from a forwarding reference:

template<typename T, typename... Ts>
constexpr auto data_of(Ts&&... _vs) {
    return data_of_t<T, Ts...>(std::forward<Ts>(_vs)...);
};

The Ts... in this case could be something like int, float const&, double&. You send those reference type and then you contain them inside of the std::tuple in data_of_t.

Those temporaries are local variables from the test function:

template <typename T = int>
class test {
public:
    static constexpr auto func(int p0=0, int p1=1, int p2=3) noexcept {
        return data_of <test<T>>
            (data_of<test<T>>(p0, p1));
    }
};

The problem here is that p0, p1, p2 are all local variable. You send them in test_of_t which will contain references to them, and you return the object containing all those reference to the local variable. This is maybe the cause of the MSVC crash. Compiler are required to provide diagnostic for any undefined behaviour in constexpr context. This crash is 100% a compiler bug and you should report it.

So how do you fix that?

Simply don't contain references by changing data_of:

template<typename T, typename... Ts>
constexpr auto data_of(Ts&&... _vs) {
    return data_of_t<T, std::decay_t<Ts>...>(std::forward<Ts>(_vs)...);
};

This will decay the type thus removing the references and decay any reference to C array to pointers.

Then, you have to change your constructor. You call std::forward in there but it's no forwarding occurring if you decay in the template arguments.

template<typename... Vs> requires((std::same_as<std::decay_t<Vs>, Ts>) && ...)
constexpr data_of_t(Vs... _vs)
    : data(std::forward<Vs>(_vs)...)
{}

This will add proper forwarding and also constrain it properly so it always do as data_of intended.

Just doing those change will remove UB from the code, but also change it a bit. The type data_of_t will always contain values, and won't contain references. If you want to send a reference, you will need something like std::ref, just like std::bind have to use to defer parameters.

You will still need to use std::forward<decltype(arg)>(arg).data for proper forwarding as @Barry stated

  • Related