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


如果我想做迭代一个元组的事情,我不得不求助于疯狂的模板元编程和模板助手专业化。例如,以下程序将不起作用:

#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<<,我不明白为什么它被禁止了。


10328
2018-06-02 21:02


起源

错误... for循环不会在编译时发生。它们必须由编译器发送到可执行文件中,然后执行它们 运行。 - uh oh somebody needs a pupper
通常,for循环不能进行编译时评估。你在谈论一个非常具体的案例。但是,相同的参数可以用于任何构造,而不仅仅用于循环。 - Oliver Charlesworth
该声明 std::get<i>(t); 没有效果。一旦你把合理的代码放在那里,你的提案的问题应该变得更加清晰。 - Kerrek SB
D 语言的 foreach 可以在编译时使用。 - coyotte508
“constexpr for”没有概念性的问题,它只是在语言中(尚未)。 - M.M


答案:


πάνταῥεῖ给出了一个很好的答案,我想提一下另一个问题 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 允许你使用给定的访问者对异类序列进行迭代 - 你可以使用它们的机制而不是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 是一次通过循环,然后在后来的传递中解除引用?应该是未定义的行为,即使它在类似的“no-constexpr for”代码中可能是合法的 std::array 上面显示?

应该的地址 y 不允许改变?编译器是否必须填充地址 y 这样,元组中最大的类型可以容纳 y?这是否意味着编译器不能简单地展开循环并开始生成代码,但必须事先展开循环的每个实例,然后从每个实例收集所有类型信息 N 实例化然后找到一个令人满意的布局?

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


8
2018-06-02 22:57



对于constexpr来说肯定是可行的,它的标准 - Sergei
@Sergei:相反,OP提出的,循环变量可以用作for循环体中的模板参数,这是不可行的 constexpr 按标准 - Chris Beck
我的意思是标准问题。没有按设计实现。可能的实现是:1)在编译时编译和启动代码2)为每次迭代实例化模板 - Sergei


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

因为一个 for() 循环用于定义 运行时控制流程 在c ++语言中。

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

 std::get<i>(t);

因此,无法在编译时推断出 i 是一个运行时变量。

使用 可变参数模板参数解包 代替。


您可能还会发现此帖子很有用(如果这甚至没有说明您的问题的答案重复):

迭代元组


4
2018-06-02 21:09



你只是说“因为它就是这样”。你还没有真正回答这个问题。 - Veedrac
@Veedrac:但这基本上就是答案 - 是因为它是定义语言的方式。 - Oliver Charlesworth
@Veedrac嗯,你认为我还应该回答什么? for() 不是为编译时构造而设计的,并且永远不会是句点。 - πάντα ῥεῖ
@OliverCharlesworth当然这是因为这就是语言的定义方式。但这是一个无用的评论,因为几乎任何问题都可以被驳回为“因为这就是语言的定义方式”。问题是关于为什么语言以这种方式定义,以及什么规则如此定义。 - Veedrac
@Veedrac不,这是纯粹的猜测。说“你必须要问标准委员会为什么”与“因为就是这样”是一样的。 - uh oh somebody needs a pupper


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

然后你可以这样做:

for_<N>([&] (auto i) {      
  std::get<i.value>(t); // do stuff
});

如果您可以访问C ++ 17编译器,则可以将其简化为

template <class F, std::size_t... Is>
void for_(F func, std::index_sequence<Is...>)
{
  (func(num<Is>{}), ...);
}

4
2017-11-29 22:44