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

use*_*815 17 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);
}
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实例中收集所有类型信息,然后找到令人满意的布局?

我认为你最好只使用一个包扩展,它更清楚如何由编译器实现它,以及它在编译和运行时的效率.

  • `for (constexpr i` 如果可能的话,可以在编译时简单地展开,即循环分解为生成的代码,每次迭代可能具有完全不同的代码。不敢相信还没有人提出它。循环变量参数和元组应该不要像atm那样痛苦。 (2认同)

Jea*_*ier 7

这是一种不需要太多样板的方法,灵感来自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)


Rom*_*tin 6

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 将被所有主要编译器在发布版本中完全实现。


πάν*_*ῥεῖ 0

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

因为C++语言中for()使用循环来定义运行时控制流。

通常,可变参数模板无法在 C++ 中的运行时控制流语句中解包。

 std::get<i>(t);
Run Code Online (Sandbox Code Playgroud)

无法在编译时推断,因为i是运行时变量。

改用可变参数模板参数解包


您可能还会发现这篇文章很有用(如果这甚至没有说明重复内容有您问题的答案):

迭代元组

  • 你只是说“因为事情就是这样”。你实际上还没有回答这个问题。 (27认同)
  • `if` 还定义了**运行时控制流**。但从 C++17 开始我们仍然有 `if constexpr`。 (15认同)
  • @Veedrac:但这本质上就是答案 - 因为这是语言的定义方式。 (7认同)
  • @OliverCharlesworth当然是因为这就是语言的定义方式。但这是一个无用的评论,因为几乎任何问题都可以被视为“因为这就是语言的定义方式”而被驳回。问题是为什么语言是这样定义的,以及什么规则是这样的。 (4认同)
  • @Veedrac 不,这纯粹是猜测。说“你必须问标准委员会为什么”与“因为事情就是这样”是一样的。 (3认同)

归档时间:

查看次数:

9181 次

最近记录:

6 年,9 月 前