13

Problem

I wish to make a function which accepts an arbitrary number of functor objects or more generally just callable objects (of different types) and applies them to an internal data structure. The function will be used with differing numbers of functors at different points in my code.

Rather than make different version to accept 1,2,3... etc and duplicate code I thought about using variadic templates.

I have found a solution which I will post below as an answer since I couldn't find anything on Google about specifically this and others might find it useful. But, please, if anyone has any better ideas post them. I have a feeling there should be a std way to do this?

Naive Attempt

I kind of knew this wouldn't work but my first attempt was

#include <iostream>
using namespace std;

struct FunctorA {
    void operator() () {
        cout << "FunctorA" << endl;
    }
};

struct FunctorB {
    void operator() () {
        cout << "FunctorB" << endl;
    }
};

template<typename... Fs>
void apply_functors(Fs... fs) {
     fs()...;   // will not work - can only expand packs in certain situations (e.g. as funciton 
}

int main(void) {
    apply_functors(FunctorA(),FunctorB());
    apply_functors(FunctorA());
    apply_functors([]()->void{ cout << "Lambda" << endl; });
    return 0;
}

However this won't work because we are only allowed to expand parameter packs in certain situations and free like this is not one.

Attempt 2

My next idea was to make a dummy function which did nothing but into which I could pass the functors as paramters then have them expanded. The reasoning behind this is that function paramters are one of the situations where we can expand paramter packs.

template<typename... Fs>
void _dummyF_impl(Fs... fs){}

template<typename... Fs>
void apply_functors(Fs... fs) {
    _dummyF_impl( fs()... );    // only works if all functors return a value (else we get "invalid use of 'void'")
}

However, this will not work because the functors return void from their operator()s and we cannot pass void as parameters to functions.

Community
  • 1
  • 1
Dan
  • 12,857
  • 7
  • 40
  • 57

3 Answers3

14

Expanding argument pack inside a braced initializer has the added benefit of guaranteed left-to-right evaluation (which is not the case for function argument list). You should also consider using perfect forwarding, here's why.

#include <initializer_list>  //
#include <utility>  // std::forward

template<typename... Fs>
void apply_functors(Fs&&... fs)
{
     auto list = { (std::forward<Fs>(fs)(), 0)... };
     //   ^^^^ deduced as std::initializer_list
}

The same thing in principle, but this time without the necessary inclusion of <initializer_list> is a variadic template constructor which you can also call with braced initializer:

struct dummy {
    template<typename... Fs>
    dummy(Fs&&... fs) { }
};

template<typename... Fs>
void apply_functors(Fs&&... fs)
{
     dummy { (std::forward<Fs>(fs)(), 0)... }; // evaluated left-to-right, too
}
Community
  • 1
  • 1
jrok
  • 54,456
  • 9
  • 109
  • 141
  • In my particular case left/right order doesn't matter but in some situations it could. I thought about using a templated struct but I'm not sure I understand how the `operator()`s get called in your example? – Dan Aug 06 '13 at 10:31
5

I would do this way

template<typename... Fs>
void apply_functors(Fs... fs) {
    int i[] = { ((void) fs(), 0)... };
    (void) i; // prevents a warning about i not being used.
}

One of the situations that parameter pack expansion occurs is inside brace initializers. Above I used it in an array initialization.

The pattern (fs(), 0)... is expanded to (f1(), 0), (f2(), 0), ..., (fn(), 0) where f1, ..., fn are the provided callable objects. Notice that evaluating the expression (f1(), 0) calls f1(), ignores its returned value and the result of the expression is the 0 (an int).

The more complicated pattern ((void) fs(), 0)... is required to prevent a corner case namely when fs() returns a type for which the comma operator is overloaded.

The extra advantage with respect to the solution that uses the dummy function is that the order in which function arguments are evaluated is not specified by the standard while in the array initialization it is (from left to right). For instance, notice that calling apply_functors(FunctorA(),FunctorB()); in Dan's solution outputs FunctorB before FunctorA whereas with this solution here the output is FunctorA followed by FunctorB.

Update: After reading jrok's solution I realized the need for perfect forwarding. So my updated solution would be

template<typename... Fs>
void apply_functors(Fs&&... fs) {
    int i[] = { ((void) std::forward<Fs>(fs)(), 0)... };
    (void) i; // prevents a warning about i not being used.
}
Community
  • 1
  • 1
Cassio Neri
  • 19,583
  • 7
  • 46
  • 68
  • 2
    Good catch on the return type with overloaded `operator ,`. That hadn't occurred to me. – Dan Aug 06 '13 at 11:00
4

My solution

The solution I found was the comma operator. I have never really had cause to use this before but it fits nicely here.

The comma operator can be used to evaluate separate statements but then "returns" the value of the last expression, so a,b,c evaluates to c.

Thus we can use this to evaluate our functors, disregard the void return value, then return something which is passed to _dummyF_impl:

template<typename... Fs>
void apply_functors(Fs... fs) {
   _dummyF_impl( (fs(),0)... );
}

This compiles fine under g++ 4.7.3 and when run the output is as we would expect:

FunctorB
FunctorA
FunctorA
Lambda
Dan
  • 12,857
  • 7
  • 40
  • 57