如果我想做迭代一个元组的事情,我不得不求助于疯狂的模板元编程和模板助手专业化。例如,以下程序将不起作用:
#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<<
不同,我无法理解为什么它不被禁止。
答案 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::fusion
或boost::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。
您可能还会发现此帖子很有用(如果这甚至没有说明您的问题有答案的副本):
答案 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)
这个“扩展声明”提案很有趣,我会提供链接供您阅读进一步的解释。
提案引入了类似于 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
的第一个参数必须是可折叠容器。