为什么此参数包中的函数调用向后求值?

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