如果我想对元组进行迭代,则必须使用疯狂的模板元编程和模板助手特化。例如,以下程序将不起作用:
#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);
}
因为
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;
}
由于
std::get<>()
是编译时构造,因此与std::cout.operator<<
不同,我无法理解为什么不允许这样做。
最佳答案
πάνταῥεῖ给出了一个很好且有用的答案,我想用constexpr for
提到另一个问题。
在C++中,在最基本的级别上,所有表达式都具有可以静态(在编译时)确定的类型。当然有类似RTTI和boost::any
这样的东西,但是它们是在此框架的基础上构建的,表达式的静态类型是理解标准中某些规则的重要概念。
假设您可以使用特殊语法来遍历异构容器,如下所示:
std::tuple<int, float, std::string> my_tuple;
for (const auto & x : my_tuple) {
f(x);
}
在这里,
f
是一些重载函数。显然,此方法的意图是为元组中的每种类型调用f
的不同重载。这实际上意味着在表达式f(x)
中,重载解析必须运行三个不同的时间。如果我们遵循C++的当前规则,那么唯一有意义的方法是在尝试弄清楚表达式的类型之前,将循环基本展开为三个不同的循环体。如果代码实际上是怎么办
for (const auto & x : my_tuple) {
auto y = f(x);
}
auto
不是魔术,它的意思不是“没有类型信息”,而是“请推断出类型,请编译”。但显然,通常确实需要三种不同类型的y
。另一方面,这种事情存在棘手的问题-在C++中,解析器需要能够知道什么名称是类型,什么名称是模板才能正确地解析语言。在解析所有类型之前,可以将解析器修改为对
constexpr for
循环进行一些循环展开吗?我不知道,但我认为这可能并不重要。也许有更好的方法...为避免此问题,在当前的C++版本中,人们使用了访问者模式。这个想法是,您将有一个重载的函数或函数对象,并将其应用于序列中的每个元素。然后,每个重载都有其自己的“主体”,因此它们中变量的类型或含义没有任何歧义。像
boost::fusion
或boost::hana
这样的库可以让您使用给定的vistior对异构序列进行迭代-您可以使用它们的机制而不是for循环。如果您可以只用整数做
constexpr for
,例如for (constexpr i = 0; i < 10; ++i) { ... }
这带来了与异构for循环相同的难度。如果可以将
i
用作主体内部的模板参数,则可以在循环主体的不同运行中创建引用不同类型的变量,然后不清楚表达式的静态类型应该是什么。因此,我不确定,但是我认为实际上向该语言添加
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;
}
在此代码中,
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;
}
关键是每次循环中
x
的类型推导都不同-由于它的类型不同,因此在堆栈上的大小和对齐方式可能不同。由于y
在堆栈之后,因此y
可能会在循环的不同运行中更改其地址-对吗?如果指向
y
的指针在循环中经过一遍,然后在下一遍中被取消引用,该怎么办?即使在上面显示的std::array
的类似“no-constexpr for”代码中可能是合法的,也应该是未定义的行为吗?不应更改
y
的地址吗?编译器是否应该填充y
的地址,以便可以在y
之前容纳元组中最大的类型?这是否意味着编译器不能简单地展开循环并开始生成代码,而是必须事先展开循环的每个实例,然后从每个N
实例化中收集所有类型信息,然后找到令人满意的布局?我认为您最好只使用pack扩展,这显然应该由编译器实现,以及在编译和运行时效率如何。
关于c++ - 为什么for循环不是编译时表达式?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/37602057/