17

What is the best way of passing a callback function parameter in C++?

I thought of simply using templates, like this:

template <typename Function>
void DoSomething(Function callback)

This is the way used e.g. in std::sort for the comparison function object.

What about passing using &&? E.g.:

template <typename Function>
void DoSomething(Function&& callback)

What are the pros and cons of those two methods, and why does the STL uses the former e.g. in std::sort?

Mr.C64
  • 41,637
  • 14
  • 86
  • 162
  • Possible duplicate of [C++11: pass (lambda or other) function object by reference or value?](http://stackoverflow.com/questions/15984499/c11-pass-lambda-or-other-function-object-by-reference-or-value) – Vittorio Romeo Nov 14 '16 at 12:29
  • 1
    @VittorioRomeo: I believe this question is *not* a duplicate at all. – Mr.C64 Nov 14 '16 at 12:29
  • This has been asked before [in one way](http://stackoverflow.com/questions/12548614/should-templated-functions-take-lambda-arguments-by-value-or-by-rvalue-reference) or [another](http://stackoverflow.com/questions/31280832/should-i-pass-a-lambda-by-const-reference)... there isn't really an "absolutely superior" way of dealing with this, it depends on the nature of the callable object. Also see [this question](http://stackoverflow.com/questions/36792059/capturing-generic-callable-objects-in-nested-lambdas-always-forward) which has similar intent. – Vittorio Romeo Nov 14 '16 at 12:36
  • 2
    My question is very different from what asked [in the other thread](http://stackoverflow.com/questions/15984499/c11-pass-lambda-or-other-function-object-by-reference-or-value). And there's nothing I should edit of course, it's just different. For example the other thread doesn't even mention passing by `&&`, and I'm curious about the pattern used by STL's `std::sort` as well (why STL does _not_ use `&&`?). – Mr.C64 Nov 14 '16 at 12:41
  • 1
    Well, a possible reason for STL passing callbacks by value is that forwarding references did not exist prior to C++11. – Vittorio Romeo Nov 14 '16 at 12:44
  • `std::sort` exists before c++11 and move forwarding reference. And that change might break existing code. – Jarod42 Nov 14 '16 at 12:44
  • Maybe the rationale or excuse for using copy with std::sort is that the passed function is a predicate which would generally be small. So performance would be better on average usage. – SVictor Nov 14 '16 at 13:21

3 Answers3

18

The

template <typename Function>
void DoSomething(Function&& callback)

… which uses a forwarding reference to pass by reference, is IMHO superior for the case where the function just uses the callback. Because a functor object, although usually small, can be arbitrarily large. Passing it by value then incurs some needless overhead.

The formal argument can end up as T&, T const& or T&& depending on the actual argument.


On the other hand, passing a simple function pointer by reference involves an extra needless indirection, which in principle could mean some slight overhead.

If in doubt whether this is significant for a given compiler, system and application, then measure.


Regarding

why does the STL uses the former [passing by value] e.g. in std::sort?

… it's worth noting that std::sort has been there since C++98, well before forwarding references were introduced in C++11, so it could not have that signature originally.

Possibly there just has not been sufficient incentive to improve this. After all, one should generally not fix that which works. Still, extra overloads with an “execution policy” argument are introduced in C++17.

Since this concerns a possible C++11 change, it's not covered by the usual source for rationales, namely Bjarne Stroustrup's “The design and evolution of C++”., and I do not know of any conclusive answer.


Using the template<class F> void call( F ) style, you can still get back "pass by reference". Simply do call( std::ref( your callback ) ). std::ref overrides operator() and forwards it to the contained object.

Similarly, with template<class F> void call( F&& ) style, you can write:

template<class T>
std::decay_t<T> copy( T&& t ) { return std::forward<T>(t); }

which explicitly copies something, and force call to use a local copy of f by:

call( copy(f) );

so the two styles mainly differ in how they behave by default.


Evg
  • 25,259
  • 5
  • 41
  • 83
Cheers and hth. - Alf
  • 142,714
  • 15
  • 209
  • 331
2

Since this is C++11 (or higher): <functional> and std::function are your best friends here.

#include <iostream>
#include <functional>
#include <string>


void DoSomething(std::function<void()> callback) {
    callback();
}

void PrintSomething() {
   std::cout << "Hello!" << std::endl;
}

int main()
{
    DoSomething(PrintSomething);
    DoSomething([]() { std::cout << "Hello again!" << std::endl; });
}
ForemanBob
  • 140
  • 1
  • 3
  • 9
    Using `std::function` introduces *potentially unnecessary* runtime overhead. Only use `std::function` if you require its runtime flexiblity *(compared to a template parameter)* and/or do not want bigger binary sizes. – Vittorio Romeo Nov 14 '16 at 12:56
  • 1
    @VittorioRomeo: I think simplicity is also one good reason for using `std::function`, i.e., that templatizing things can often be premature optimization. For example, a scope guard class template is trivial to express with `std::function`. To express it with generic function type one needs to introduce a factory function. – Cheers and hth. - Alf Nov 14 '16 at 13:40
  • 1
    As Vittorio said, std::function causes a potential runtime overhead. This is why I downvoted your suggestion that this should be your default choice. – user3721426 Nov 14 '16 at 19:51
  • The runtime overhead is acceptable to non-constrained env. and getting smaller. – Jojje Nov 16 '16 at 08:37
0

In my opinion, using the && would be the superior way, basically because it avoids copy constructing the functor when the argument is an lvalue. In case of a lambda, this means avoiding to copy construct all captured values. In case of an std::function you also have an additional heap allocation unless small object optimizations are used.

But there are some cases when it could be a good thing to have a copy of the functor. One example is when using a generator function object like this:

#include <functional>
#include <cstdio>

template <typename F>
void func(F f) {
        for (int i = 0; i < 5; i++) {
                printf("Got %d\n", (int)f());
        }
}

int main() {
        auto generator0 = [v = 0] () mutable { return v++; };
        auto generator1 = generator0; generator1();

        printf("Printing 0 to 4:\n");
        func(generator0);
        printf("Printing 0 to 4 again:\n");
        func(generator0);
        printf("Printing 1 to 5:\n");
        func(generator1);
}

If we used F&& instead, the second group of prints would print values 5 to 9 instead of 0 to 4. I guess it's up to the application/library what behaviour is preferred.

When using function pointers, we could also end up in a situation when an extra level of indirection must be used in the invoke2 function:

template <typename F>
void invoke1(F f) {
        f();
}

template <typename F>
void invoke2(F&& f) {
        f();
}

void fun();

void run() {
        void (*ptr)() = fun;
        void (&ref)() = fun;

        invoke1(fun); // calls invoke1<void (*)()>(void (*)())
        invoke1(&fun);// calls invoke1<void (*)()>(void (*)())
        invoke1(ptr); // calls invoke1<void (*)()>(void (*)())
        invoke1(ref); // calls invoke1<void (*)()>(void (*)())

        invoke2(fun); // calls invoke2<void (&)()>(void (&)())
        invoke2(&fun);// calls invoke2<void (*)()>(void (*&&)())
        invoke2(ptr); // calls invoke2<void (*&)()>(void (*&)())
        invoke2(ref); // calls invoke2<void (&)()>(void (&)())
}

The statement invoke2(&fun); puts the address to the function on the stack, then sends the reference (i.e. an address) to this temporary stack slot to the invoke2 function. The invoke2 function must then use an extra memory lookup to read the reference on the stack before it can use it. The extra indirection applies for invoke2(ptr) too. In all other cases, the address to the function is sent directly to the invoke1/invoke2 function and does not need any extra indirection.

This example shows one interesting difference between a function reference and a function pointer.

Of course, compiler optimisation such as inlining could easily get rid of this extra indirection.

Emil
  • 16,784
  • 2
  • 41
  • 52