如何通过标准元组操作正确转发和使用 constexpr 结构的嵌套元组
How to correctly forward and use a nested tuple of constexpr struct with standard tuple operations
我想通过 struct
的 constexpr
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
限定符有关。
如何解决这个问题?
这个:
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));
}
};
这里的问题是p0
、p1
、p2
都是局部变量。您将它们发送到 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
进行正确转发
我想通过 struct
的 constexpr
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
限定符有关。
如何解决这个问题?
这个:
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));
}
};
这里的问题是p0
、p1
、p2
都是局部变量。您将它们发送到 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
进行正确转发