How to guarantee order of argument evaluation when calling a function object?
The answers to the question on how to avoid undefined execution order for the constructors when using std::make_tuple led to a discussion during which I learned that the order of argument evaluation can be guaranteed for constructors: Using a braced-init-list the order is guaranteed to be left to right:
T{ a, b, c }
The expressions a
, b
, and c
are evaluated in the given order. This is the case, even if the type T
just has a normal constructor defined.
Clearly, not everything called is a constructor and sometimes it would be nice to guarantee the order of evaluation when calling a function but there is no such thing as brace-argument-list to call function with a defined order of evaluation of their arguments. The question becomes: Can the guarantees to constructors be used to build a function call facility ("function_apply()
") with an ordering guarantee for the evaluation of arguments? It is acceptable to require a function object being called.
What about a silly wrapper class like this:
struct OrderedCall
{
template <typename F, typename ...Args>
OrderedCall(F && f, Args &&... args)
{
std::forward<F>(f)(std::forward<Args>(args)...);
}
};
Usage:
void foo(int, char, bool);
OrderedCall{foo, 5, 'x', false};
If you want a return value, you could pass it in by reference (you'll need some trait to extract the return type), or store it in the object, to get an interface like:
auto x = OrderedCall{foo, 5, 'x', false}.get_result();
The solution I had come up uses std::tuple<...>
to put the arguments together than calls a function object using the elements of this object. The advantage is that it can deduce the return type. The actual specific logic looks like this:
template <typename F, typename T, int... I>
auto function_apply(F&& f, T&& t, indices<I...> const*)
-> decltype(f(std::get<I>(t)...)) {
f(std::get<I>(t)...);
}
template <typename F, typename T>
auto function_apply(F&& f, T&& t)
-> decltype(function_apply(std::forward<F>(f), std::forward<T>(t),
make_indices<T>())) {
function_apply(std::forward<F>(f), std::forward<T>(t),
make_indices<T>());
}
... which is called using an expression like this:
void f(int i, double d, bool b) {
std::cout << "i=" << i << " d=" << d << " b=" << b << '\n';
}
int fi() { std::cout << "int\n"; return 1; }
double fd() { std::cout << "double\n"; return 2.1; }
bool fb() { std::cout << "bool\n"; return true; }
int main()
{
std::cout << std::boolalpha;
function_apply(&f, std::tuple<int, double, bool>{ fi(), fd(), fb() });
}
The main disadvantage is that this approach requires the specification of the std::tuple<...>
's elements. Another problem is that the current version of gcc on MacOS calls the functions in the opposite order they appear, i.e., the order of evaluation in a braced-init-list isn't obeyed (a gcc bug) or doesn't exist (i.e., I misunderstood the guarantees of using a braced-init-list. clang on the same platform executes the functions in the expected order.
The used function make_indices()
just creates a suitable pointer to an object of type indices<I...>
with a list of indices usable with a std::tuple<...>
:
template <int... Indices> struct indices;
template <> struct indices<-1> { typedef indices<> type; };
template <int... Indices>
struct indices<0, Indices...>
{
typedef indices<0, Indices...> type;
};
template <int Index, int... Indices>
struct indices<Index, Indices...>
{
typedef typename indices<Index - 1, Index, Indices...>::type type;
};
template <typename T>
typename indices<std::tuple_size<T>::value - 1>::type const*
make_indices()
{
return 0;
}