为什么此参数包中的函数调用向后求值?
Why are function calls in this parameter pack evaluated backwards?
最近我发现 this Whosebug answer 关于使用模板展开循环。答案是“这个想法适用于 C++11”,我最后得到的是:
namespace tmpl {
namespace details {
template<class T, T... values>
class integer_sequence {
public:
static constexpr size_t size() { return sizeof...(values); }
};
template<class T, class N, class... Is>
struct make_integer_sequence_helper :
make_integer_sequence_helper<T, std::integral_constant<T, N::value - 1>, std::integral_constant<T, N::value - 1>, Is...> {};
template<class T, class... Is>
struct make_integer_sequence_helper<T, std::integral_constant<T, 0>, Is...> {
using type = integer_sequence<T, Is::value...>;
};
template<class T, T N>
using make_integer_sequence = typename make_integer_sequence_helper<T, std::integral_constant<T, N>>::type;
template<class... Ts>
void variadic_noop(Ts... params) {}
template<class F, class T>
int call_and_return_0(F&& f, T i) {f(i); return 0;}
template<class T, T... Is, class F>
void loop(integer_sequence<T, Is...>, F&& f) {
variadic_noop(call_and_return_0(f, Is)...);
}
}
template<class T, T max, class F>
void loop(F&& f) {
details::loop(details::make_integer_sequence<T, max>{}, f);
}
}
让我们举一个简单的例子来说明如何使用这个模板:
tmpl::loop<size_t, 20>([&](size_t idx) {
cout << "Loop " << idx << std::endl;
});
当我使用其他答案中的 C++17 代码时,它从 0 迭代到 19。但是,我编写的 C++11 jank 从 19 迭代到 0。
理论上,details::loop()
展开后应该是这样的:
variadic_noop(call_and_return_0(f, 0), call_and_return_0(f, 1), call_and_return_0(f, 2), ...);
那么,如果 C++ 运行 call_and_return_0(f, 19)
是 variadic_noop()
的最后一个参数,为什么它首先出现?
函数调用中函数参数的计算可以是从左到右、从右到左或任何其他顺序,这不需要是可预测的。
但是,如果您有 形式为 e_1, e_2, ..., e_n
的单个表达式 ,其中子表达式 e_1
、e_2
、..., e_n
由 逗号运算符分隔 ,那么子表达式将始终按从左到右的顺序求值,因为逗号运算符保证其左操作数在其右操作数之前求值操作数。 (在 C++17 之前,此保证仅适用于内置逗号运算符,不适用于任何重载的逗号运算符。)
虽然函数调用也使用逗号,但函数调用中的逗号并不强制执行从左到右的求值。链接答案中的代码使用 C++17 折叠表达式创建由逗号分隔的表达式序列 operators.
加上 Brian 所说的,可靠地获得顺序的唯一方法是将循环编写为递归模板。实施示例:
template <class T, T I, T... Is, class F>
void loop(integer_sequence<T, I, Is...>, F&& f) {
f(I);
loop(integer_sequence<T, Is...> {}, f);
}
template<class T, class F>
void loop(integer_sequence<T>, F&& f) {}
编辑: 我再次查看了包扩展发生的位置,以及 this Whosebug answer。要回避函数求值顺序问题,您可以使用花括号初始化列表,它保证从左到右的顺序。它还将允许更大的序列,因为不再需要递归来展开循环。
inline void init_list_noop(std::initializer_list<int>) {}
template<class T, T val, class F>
int call_and_return_0(F&& f) {
f(val);
return 0;
}
template<class T, T... vals, class F>
void loop(integer_sequence<T, vals...>, F&& f) {
init_list_noop({call_and_return_0<T, vals>(f)...});
}
最近我发现 this Whosebug answer 关于使用模板展开循环。答案是“这个想法适用于 C++11”,我最后得到的是:
namespace tmpl {
namespace details {
template<class T, T... values>
class integer_sequence {
public:
static constexpr size_t size() { return sizeof...(values); }
};
template<class T, class N, class... Is>
struct make_integer_sequence_helper :
make_integer_sequence_helper<T, std::integral_constant<T, N::value - 1>, std::integral_constant<T, N::value - 1>, Is...> {};
template<class T, class... Is>
struct make_integer_sequence_helper<T, std::integral_constant<T, 0>, Is...> {
using type = integer_sequence<T, Is::value...>;
};
template<class T, T N>
using make_integer_sequence = typename make_integer_sequence_helper<T, std::integral_constant<T, N>>::type;
template<class... Ts>
void variadic_noop(Ts... params) {}
template<class F, class T>
int call_and_return_0(F&& f, T i) {f(i); return 0;}
template<class T, T... Is, class F>
void loop(integer_sequence<T, Is...>, F&& f) {
variadic_noop(call_and_return_0(f, Is)...);
}
}
template<class T, T max, class F>
void loop(F&& f) {
details::loop(details::make_integer_sequence<T, max>{}, f);
}
}
让我们举一个简单的例子来说明如何使用这个模板:
tmpl::loop<size_t, 20>([&](size_t idx) {
cout << "Loop " << idx << std::endl;
});
当我使用其他答案中的 C++17 代码时,它从 0 迭代到 19。但是,我编写的 C++11 jank 从 19 迭代到 0。
理论上,details::loop()
展开后应该是这样的:
variadic_noop(call_and_return_0(f, 0), call_and_return_0(f, 1), call_and_return_0(f, 2), ...);
那么,如果 C++ 运行 call_and_return_0(f, 19)
是 variadic_noop()
的最后一个参数,为什么它首先出现?
函数调用中函数参数的计算可以是从左到右、从右到左或任何其他顺序,这不需要是可预测的。
但是,如果您有 形式为 e_1, e_2, ..., e_n
的单个表达式 ,其中子表达式 e_1
、e_2
、..., e_n
由 逗号运算符分隔 ,那么子表达式将始终按从左到右的顺序求值,因为逗号运算符保证其左操作数在其右操作数之前求值操作数。 (在 C++17 之前,此保证仅适用于内置逗号运算符,不适用于任何重载的逗号运算符。)
虽然函数调用也使用逗号,但函数调用中的逗号并不强制执行从左到右的求值。链接答案中的代码使用 C++17 折叠表达式创建由逗号分隔的表达式序列 operators.
加上 Brian 所说的,可靠地获得顺序的唯一方法是将循环编写为递归模板。实施示例:
template <class T, T I, T... Is, class F>
void loop(integer_sequence<T, I, Is...>, F&& f) {
f(I);
loop(integer_sequence<T, Is...> {}, f);
}
template<class T, class F>
void loop(integer_sequence<T>, F&& f) {}
编辑: 我再次查看了包扩展发生的位置,以及 this Whosebug answer。要回避函数求值顺序问题,您可以使用花括号初始化列表,它保证从左到右的顺序。它还将允许更大的序列,因为不再需要递归来展开循环。
inline void init_list_noop(std::initializer_list<int>) {}
template<class T, T val, class F>
int call_and_return_0(F&& f) {
f(val);
return 0;
}
template<class T, T... vals, class F>
void loop(integer_sequence<T, vals...>, F&& f) {
init_list_noop({call_and_return_0<T, vals>(f)...});
}