I believe we need a guideline for accepting function object parameters in functions that model some form of control flow, and particularly in functions modeling loops of some kind.
Let's start with the example currently in F.24:
template <class F, class... Args>
inline auto invoke(F&& f, Args&&... args) {
return forward<F>(f)(forward<Args>(args)...);
}
Now let's modify it slightly to bring the issue out:
template <class F, class... Args>
inline void invoke_n(int n, F&& f, Args&&... args) {
for (int i = 0; i < n; ++i) {
forward<F>(f)(forward<Args>(args)...);
}
}
I come across situations like this all the time where I am trying to model some form of loop as a higher order function.
The question is: how should functions like invoke_n
accept the function object parameter (f
in this case)?
We can rule out a few options right away. invoke_n
can't accept f
as any form of l-value reference or as any form of const reference. A non-const l-value reference would prevent temporary objects from being passed as the function argument, which is particularly bad for lambdas. Any const reference would prevent mutable function objects from being passed, which is also undesirable.
That leaves us with two options: to accept f
by forwarding reference, as in the example above, or to accept f
by value. Both of these have possible issues:
By forwarding reference
template <class F, class... Args>
inline void invoke_n(int n, F&& f, Args&&... args) {
for (int i = 0; i < n; ++i) {
forward<F>(f)(forward<Args>(args)...);
}
}
This has a number of advantages:
- It accepts any form of function object exactly as we would expect it to.
- It feels right conceptually: all we're trying to do is just "inject" some code into a different context. This does that as purely as possible.
- It always forwards its forwarding reference as recommended in the guidelines, so it's a consistent rule to follow.
It also has a possible very odd disadvantage: it forwards its function object (and indeed, its parameter pack) multiple times. This feels really dirty, since it means that we will be (possibly) using a single object as an r-value over and over again. This is potentially problematic since we can ref qualify functions and the writer of a function object class may not expect an r-value qualified member function to be called repeatedly. This is likely not an issue for lambdas, but could be for other function objects. Indeed, one could see extending this issue to simply ask about forwarding any kind of object multiple times (one could envision a function like make_n
that constructs n
copies of some type and returns them in a vector
, for example; indeed, this very example forwards its parameter pack multiple times, which could get hairy).
There's another option, of course:
template <class F, class... Args>
inline void invoke_n(int n, F&& f, Args&&... args) {
for (int i = 0; i < n; ++i) {
f(forward<Args>(args)...);
}
}
This avoids the problem of forwarding f
multiple times (though it still forwards its parameter pack multiple times). On the other hand, it treats a possible r-value reference like an l-value and it creates a corner case for an otherwise consistent rule: here we are not forwarding a forwarding reference. This also feels gross.
By value
template <class F, class... Args>
inline void invoke_n(int n, F f, Args&&... args) {
for (int i = 0; i < n; ++i) {
f(forward<Args>(args)...);
}
}
This avoids all the reference issues above, but has two major downsides:
- It relies heavily on the optimizer's inlining ability to elide copies. While perhaps a reasonable expectation, it doesn't feel great to copy a function object just to model a loop.
- It won't work with any function object that wants to mutate itself and allow the caller to read those results. That's not an issue with lambdas, but it would be with a good deal of plausible hand-rolled function objects.
In short, I'm stumped as to how to handle this case. I guess my knee jerk reaction is to simply forward every forwarding reference every time, but I can see that causing problems. Some guidance on this issue would be greatly appreciated, and it may even need to be extended to guidance for more than just function objects; forwarding the parameter pack in this example is equally problematic. It's the function object that I find most interesting, however.