c++ - 为什么for循环不是编译时表达式?

标签 c++ constexpr

如果我想对元组进行迭代,则必须使用疯狂的模板元编程和模板助手特化。例如,以下程序将不起作用:

#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::fusionboost::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/

相关文章:

c++ - 静态枚举忽略类内的警告

c++ - 为什么 constexpr 不是所有函数的默认值?

c++ - constexpr 未在 VC2013 中编译

c++ - 为什么必须在运行时构造字符串?

C++ 对齐和数组

c++ - 摆脱 QTableWidgetItem 中的单元格小部件

c++ - std::vector::erase(iterator position) 不一定调用相应元素的析构函数

c++ - 在 C++ 中将实用程序 DLL 拆分为更小的组件

c++ - 为什么强制 constexpr 只有编译时错误?

c++ - 模板非类型模板参数