如何通过标准元组操作正确转发和使用 constexpr 结构的嵌套元组

How to correctly forward and use a nested tuple of constexpr struct with standard tuple operations

我想通过 structconstexpr constructor 存储传递的数据,并将数据存储在 std::tuple 中,以执行各种 TMP/编译时操作.

实施

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

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

存储的数据使用轻量级 strong type 结构,通过左值和右值助手重载生成:

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)...);
};

实现方式如

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);
}

进入正题

std::tuple_cat 如果 myInitializer 实例是 constexpr.

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

它似乎与通过 constexpr 添加的 const 限定符有关。

如何解决这个问题?

查看完整示例 https://godbolt.org/z/j5xdT39aE

这个:

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

不是转发数据的正确方式。 decltype(args.data) 将为您提供该数据成员的类型 - 这不是 const-ness 或 args 的值类别的函数。让我们举一个更简单的例子:

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});

所以这里我调用了三个 f(分别调用 f<C&>f<const C&>f<C>)。在所有这三种情况下,decltype(arg.data) 都...只是 int。这就是 C::data 的类型。但这 不是 它需要如何转发(它不会为 c2 编译,因为我们正试图抛弃 const-ness - 正如你的例子-- 它会错误地移出 c1).

你要的是单独转发arg,然后然后访问数据:

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

现在,decltype(arg) 实际上因实例化而异,这很好地表明我们正在做一些明智的事情。

除了 Barry 指出的转发问题之外,还有一个不同的原因导致您不能在 init 上使用 constexpr。这是因为您在 data_of_t.

中包含对临时文件的引用

你看,你正在包含一个从转发引用的重载解析中获得的类型:

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

本例中的 Ts... 可能类似于 int, float const&, double&。您发送这些引用类型,然后将它们包含在 data_of_t.

中的 std::tuple

这些临时变量是来自 test 函数的局部变量:

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));
    }
};

这里的问题是p0p1p2都是局部变量。您将它们发送到 test_of_t 中,其中将包含对它们的引用,并且您 return 包含所有这些对局部变量的引用的对象。这可能是 MSVC 崩溃的原因。编译器需要为 constexpr 上下文中的任何未定义行为提供诊断。此崩溃 100% 是编译器错误,您应该报告它。

那么你如何解决这个问题?

只需更改 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)...);
};

这将衰减类型从而删除引用并将对 C 数组的任何引用衰减为指针。

然后,你必须改变你的构造函数。你在那里调用 std::forward 但如果你在模板参数中衰减则不会发生转发。

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

这将添加适当的转发并对其进行适当的约束,因此它始终按 data_of 预期的方式进行。

只需进行这些更改即可从代码中删除 UB,但也会对其进行一些更改。 data_of_t 类型将始终包含值,而不包含引用。如果你想发送一个引用,你将需要像 std::ref 这样的东西,就像 std::bind 必须使用延迟参数一样。

如@Barry 所述

,您仍然需要使用 std::forward<decltype(arg)>(arg).data 进行正确转发