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

时间:2016-06-02 21:02:33

标签: 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<<不同,我无法理解为什么它不被禁止。

5 个答案:

答案 0 :(得分:8)

πάνταῥεῖ给出了一个很好而有用的答案,我想用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这样的库允许您使用给定的访问者对异构序列进行迭代 - 您将使用它们的机制而不是for循环。

如果你只需要整理constexpr for,例如

for (constexpr i = 0; i < 10; ++i) { ... }

这引起了与异类for循环相同的困难。如果你可以在body中使用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实例中收集所有类型信息,然后查找一个满意的布局?

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

答案 1 :(得分: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>());
}

然后你可以这样做:

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>{}), ...);
}

答案 2 :(得分:4)

  

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

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

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

 std::get<i>(t);

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

改为使用variadic template parameter unpacking

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

iterate over tuple

答案 3 :(得分:0)

C ++ 20 中,大多数std::algorithm函数将是constexpr。例如,使用std::transform,可以在编译时完成许多需要循环的操作。考虑以下示例在编译时计算数组中每个数字的阶乘(改编自Boost.Hana documentation):

#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}}, "");
}

查看如何在编译时使用“ 循环”(即facts)来计算数组std::algorithm。在撰写本文时,您需要最新的clang或gcc版本的实验版本,可以在godbolt.org上进行尝试。但是很快C ++ 20将由发行版本中的所有主要编译器完全实现。

答案 4 :(得分:0)

这个“扩展声明”提案很有趣,我会提供链接供您阅读进一步的解释。

Click this link

提案引入了类似于 for... 运算符的语法糖 sizeof...for... 循环语句是一个编译时表达式,这意味着它与运行时无关。

例如:

std::tuple<int, float, char> Tup1 {5, 3.14, 'K'};
for... (auto elem : Tup1) {
     std::cout << elem << " "; 
}

编译器会在编译时生成代码,这是等价的:

std::tuple<int, float, char> Tup1 {5, 3.14, 'K'};
{
  auto elem = std::get<0>(Tup1);
  std::cout << elem << " ";
}
{
  auto elem = std::get<1>(Tup1);
  std::cout << elem << " ";
}
{
  auto elem = std::get<2>(Tup1);
  std::cout << elem << " ";
}

因此,扩展语句不是循环,而是循环体的重复版本,正如文档中所说。

因为这个提议不在 C++ 的当前版本或技术规范中(如果它被接受)。我们可以专门使用 boost 库中的替代版本 <boost/hana/for_each.hpp>,并使用 <boost/hana/tuple.hpp> 中 boost 的元组版本。单击此 link

#include <boost/hana/for_each.hpp>
#include <boost/hana/tuple.hpp>
using namespace boost;

...

hana::tuple<int, std::string, float> Tup1 {5, "one", 5.55};
hana::for_each(Tup1, [](auto&& x){
    std::cout << x << " ";
});

// Which will print:
// 5 "one" 5.55

boost::hana::for_each 的第一个参数必须是可折叠容器。