如果我想做迭代一个元组的事情,我不得不求助于疯狂的模板元编程和模板助手专业化.例如,以下程序将不起作用:
#include <iostream>
#include <tuple>
#include <utility>
constexpr auto multiple_return_values()
{
return std::make_tuple(3, 3.14, "pi");
}
template <typename T>
constexpr void foo(T t)
{
for (auto i = 0u; i < std::tuple_size<T>::value; ++i)
{
std::get<i>(t);
}
}
int main()
{
constexpr auto ret = multiple_return_values();
foo(ret);
}
Run Code Online (Sandbox Code Playgroud)
因为i不能const或我们无法实现它.但是for循环是一个可以静态计算的编译时构造.由于as-if规则,编译器可以自由地删除它,转换它,折叠它,展开它或者用它做任何他们想做的事情.但是为什么不能以constexpr方式使用循环呢?这段代码中没有任何东西需要在"运行时"完成.编译器优化就是证明.
我知道你可能会i在循环体内修改,但编译器仍然可以检测到它.例:
// ...snip...
template <typename T>
constexpr int foo(T t)
{
/* Dead code */
for (auto i = 0u; i < std::tuple_size<T>::value; ++i)
{
}
return 42;
}
int main()
{
constexpr auto ret = multiple_return_values();
/* No error */
std::array<int, foo(ret)> arr;
}
Run Code Online (Sandbox Code Playgroud)
由于std::get<>()是一个编译时构造,不像std::cout.operator<<,我不明白为什么它被禁止.
Chr*_*eck 10
πάνταῥεῖ给出了一个很好的答案,我想提一下另一个问题constexpr for.
在C++中,在最基本的层面上,所有表达式都有一个可以静态确定的类型(在编译时).有一些像RTTI这样的东西boost::any,当然,它们建立在这个框架之上,而表达式的静态类型是理解标准中某些规则的重要概念.
假设您可以使用花哨的语法迭代异构容器,例如:
std::tuple<int, float, std::string> my_tuple;
for (const auto & x : my_tuple) {
f(x);
}
Run Code Online (Sandbox Code Playgroud)
这里f是一些重载函数.显然,这意味着为f元组中的每个类型调用不同的重载.这实际上意味着在表达式中f(x),重载决策必须运行三次.如果我们按照当前的C++规则进行游戏,那么唯一可以理解的方法是,在我们尝试找出表达式的类型之前,我们基本上将循环展开为三个不同的循环体.
如果代码实际上怎么办?
for (const auto & x : my_tuple) {
auto y = f(x);
}
Run Code Online (Sandbox Code Playgroud)
auto不是魔术,它并不意味着"没有类型信息",它的意思是"推断类型,请,编译器".但显然,y一般来说确实需要三种不同的类型.
另一方面,这种事情存在棘手的问题 - 在C++中,解析器需要能够知道哪些名称是类型,哪些名称是模板才能正确解析语言.在解析constexpr for所有类型之前,是否可以修改解析器以进行循环的循环展开?我不知道,但我认为这可能是不平凡的.也许还有更好的方法......
为避免此问题,在当前版本的C++中,人们使用访问者模式.这个想法是你将有一个重载的函数或函数对象,它将应用于序列的每个元素.然后每个重载都有自己的"主体",因此它们中的变量的类型或含义没有歧义.还有像图书馆boost::fusion或者boost::hana是让你做遍历所有使用给定vistior异质序列-你会用,而不是一个for循环的机制.
如果你可以constexpr for只用整数,例如
for (constexpr i = 0; i < 10; ++i) { ... }
Run Code Online (Sandbox Code Playgroud)
这引起了与异类for循环相同的困难.如果你可以i在body中使用模板参数,那么你可以在循环体的不同运行中创建引用不同类型的变量,然后不清楚表达式的静态类型应该是什么.
所以,我不确定,但我认为可能存在一些与实际constexpr for向该语言添加功能相关的重要技术问题.访客模式/计划的反映功能可能最终不会让人头疼IMO ...谁知道.
让我举一个我刚才想到的例子,说明了所涉及的困难.
在普通的C++中,编译器知道堆栈上每个变量的静态类型,因此它可以计算该函数的堆栈帧的布局.
您可以确保在执行函数时,局部变量的地址不会更改.例如,
std::array<int, 3> a{{1,2,3}};
for (int i = 0; i < 3; ++i) {
auto x = a[i];
int y = 15;
std::cout << &y << std::endl;
}
Run Code Online (Sandbox Code Playgroud)
在此代码中,y是for循环体中的局部变量.它在整个函数中都有一个明确定义的地址,每次编译器打印的地址都是相同的.
使用constexpr的类似代码应该是什么行为?
std::tuple<int, long double, std::string> a{};
for (int i = 0; i < 3; ++i) {
auto x = std::get<i>(a);
int y = 15;
std::cout << &y << std::endl;
}
Run Code Online (Sandbox Code Playgroud)
关键是x在循环的每次传递中推导出的类型不同 - 因为它具有不同的类型,它可能在堆栈上具有不同的大小和对齐.由于y它在堆栈之后,这意味着y可能会在循环的不同运行中更改其地址 - 对吗?
如果y在一次循环中获取指针,然后在稍后的传递中取消引用,那么该行为应该是什么?它应该是未定义的行为,即使它在std::array上面显示的类似"no-constexpr for"代码中可能是合法的吗?
不应该改变地址y吗?编译器是否必须填充地址,y以便可以容纳元组中最大的类型y?这是否意味着编译器不能简单地展开循环并开始生成代码,但必须事先展开循环的每个实例,然后从每个N实例中收集所有类型信息,然后找到令人满意的布局?
我认为你最好只使用一个包扩展,它更清楚如何由编译器实现它,以及它在编译和运行时的效率.
这是一种不需要太多样板的方法,灵感来自http://stackoverflow.com/a/26902803/1495627:
template<std::size_t N>
struct num { static const constexpr auto value = N; };
template <class F, std::size_t... Is>
void for_(F func, std::index_sequence<Is...>)
{
using expander = int[];
(void)expander{0, ((void)func(num<Is>{}), 0)...};
}
template <std::size_t N, typename F>
void for_(F func)
{
for_(func, std::make_index_sequence<N>());
}
Run Code Online (Sandbox Code Playgroud)
然后你可以这样做:
for_<N>([&] (auto i) {
std::get<i.value>(t); // do stuff
});
Run Code Online (Sandbox Code Playgroud)
如果您可以访问C++ 17编译器,则可以将其简化为
template <class F, std::size_t... Is>
void for_(F func, std::index_sequence<Is...>)
{
(func(num<Is>{}), ...);
}
Run Code Online (Sandbox Code Playgroud)
在C++20 中,大多数std::algorithm函数都是constexpr. 例如使用std::transform,许多需要循环的操作可以在编译时完成。考虑这个在编译时计算数组中每个数字的阶乘的例子(改编自Boost.Hana 文档):
#include <array>
#include <algorithm>
constexpr int factorial(int n) {
return n == 0 ? 1 : n * factorial(n - 1);
}
template <typename T, std::size_t N, typename F>
constexpr std::array<std::result_of_t<F(T)>, N>
transform_array(std::array<T, N> array, F f) {
auto array_f = std::array<std::result_of_t<F(T)>, N>{};
// This is a constexpr "loop":
std::transform(array.begin(), array.end(), array_f.begin(), [&f](auto el){return f(el);});
return array_f;
}
int main() {
constexpr std::array<int, 4> ints{{1, 2, 3, 4}};
// This can be done at compile time!
constexpr std::array<int, 4> facts = transform_array(ints, factorial);
static_assert(facts == std::array<int, 4>{{1, 2, 6, 24}}, "");
}
Run Code Online (Sandbox Code Playgroud)
查看如何facts在编译时使用“循环”(即std::algorithm. 在撰写本文时,您需要最新的 clang 或 gcc 版本的实验版本,您可以在Godbolt.org上试用。但很快 C++20 将被所有主要编译器在发布版本中完全实现。
为什么 for 循环不是编译时表达式?
因为C++语言中for()使用循环来定义运行时控制流。
通常,可变参数模板无法在 C++ 中的运行时控制流语句中解包。
std::get<i>(t);
Run Code Online (Sandbox Code Playgroud)
无法在编译时推断,因为i是运行时变量。
您可能还会发现这篇文章很有用(如果这甚至没有说明重复内容有您问题的答案):
| 归档时间: |
|
| 查看次数: |
9181 次 |
| 最近记录: |