Home > Software design >  Why are function calls in this parameter pack evaluated backwards?
Why are function calls in this parameter pack evaluated backwards?

Time:11-02

Recently I found this StackOverflow answer about unrolling a loop with templates. The answer states that "the idea is applicable to C 11", and I ended up with this:

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

Let's take a simple example of how this template would be used:

tmpl::loop<size_t, 20>([&](size_t idx) {
  cout << "Loop " << idx << std::endl;
});

When I use the C 17 code from the other answer, it iterates from 0 up to 19. However, the C 11 jank I've written iterates from 19 down to 0.

In theory, when details::loop() is expanded it should become something like this:

variadic_noop(call_and_return_0(f, 0), call_and_return_0(f, 1), call_and_return_0(f, 2), ...);

So, why does C run call_and_return_0(f, 19) first if it's the last parameter to variadic_noop()?

CodePudding user response:

The evaluation of function arguments in a function call may be from left to right, right to left, or any other order, which is not required to be predictable.

However, when you have a single expression of the form e_1, e_2, ..., e_n where the subexpressions e_1, e_2, ..., e_n are separated by comma operators, then the subexpressions will always be evaluated in left-to-right order, because the comma operator guarantees that its left operand is evaluated before its right operand. (Prior to C 17, this guarantee only holds for the built-in comma operator, and not for any overloaded ones.)

Although function calls also use commas, the commas in a function call do not enforce left-to-right evaluation. The code from the linked answer uses C 17 fold expressions to create a sequence of expressions separated by comma operators.

CodePudding user response:

Adding onto what Brian said, the only way to get the order reliably is to write the loop as a recursive template. Sample implementation:

    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) {}
  • Related