2

I have a function which takes an object and invokes various callback functions on it as its output. Example:

template<typename T>
void iterateInParallel(vector<int> const& a, vector<int> const& b, T && visitor)
{
    // sometimes invokes visitor.fromA(i)
    // sometimes invokes visitor.fromB(i)
    // sometimes invokes visitor.fromBoth(i, j)
}

(Don't get caught up in exactly what the function does, it's an example and not relevant here.)

Now, when writing code to use a function which takes a callback object, I'll often use a local lambda with a default by-ref capture to keep my code tight and readable.

int sum = 0;
int numElems = 0;
someFunctionTakingACallback(args, [&](int x) {sum += x; numElems++; });

That's a lot nicer than the old functor approach, where I'd define a class for the purpose which took refs to sum and numElems in its constructor. And like the functor approach, it generally has zero runtime cost, since the code for the callback is known when instantiating the function.

But a lambda is but one function, so it's not workable for the iterateInParallel method above; I'm back to copying refs around.

So what I'm looking for, is a means of getting roughly the level of convenience, performance, and idiomaticity I have from lambdas' default-capture, while still being able to invoke multiple types of callbacks.

Options I've considered:

  • Passing multiple callback functions. This isn't too bad, and is usually what I've gone for. But it's easy to get the parameter order mixed up, and it can really bloat the argument list. It's also less self-documenting, since the name of the callbacks are nowhere in the user code.
  • Rewriting the inner function so that it invokes one callback, with parameters to help define what kind of callback it is. Ugly, hacky, and not the sort of thing a respectable programmer would do.
  • Passing a structure with a whole bunch of std::functions in it. I get the feeling that I could get this to look okay, but I also get the feeling that it would end up doing heap allocations and per-invocation indirection.
  • Something wacky involving the preprocessor. Heaven forfend.
YSC
  • 38,212
  • 9
  • 96
  • 149
Sneftel
  • 40,271
  • 12
  • 71
  • 104

1 Answers1

4

Deduction guides (C++17) and designated initialization (c++20) together provide a suitable solution:

#include <iostream>

template<class F1, class F2>
struct Visitor
{
    F1 fromA;
    F2 fromB;
};
template<class F1, class F2>
Visitor(F1, F2) -> Visitor<F1, F2>;


template<class F1, class F2>
void f(Visitor<F1, F2> v)
{
    v.fromA(1);
    v.fromB("hello");
}


int main()
{
    f( Visitor{
        .fromA = [](int n){ std::cout << "A: " << n << "\n"; },
        .fromB = [](std::string_view v){ std::cout << "B: " << v << "\n"; }
    });
}

(demo)

YSC
  • 38,212
  • 9
  • 96
  • 149
  • That's not bad, but requires a standard revision that isn't complete yet. Also, seems like that prevents `f` from taking a "normal" functor. – Sneftel Jun 07 '18 at 11:04
  • And *concept* is missing to restrict `F1`/`F2` ;-) – Jarod42 Jun 07 '18 at 11:04
  • @Jarod42 you're pass the idiom here ;) But yes. – YSC Jun 07 '18 at 11:05
  • @Sneftel: `F1`/`F2` can be regular functors. and if you use `std::invoke` instead of `operator()` in `f`, you could even pass pointer on method :-) – Jarod42 Jun 07 '18 at 11:10
  • @Jarod42 Sorry, I should have said "a normal struct". As in, `f` is no longer fully parametric, but requires a visitor which is an instantiation of `Visitor`. – Sneftel Jun 07 '18 at 11:16
  • So you may change `f` to `template void f(T&&);`. – Jarod42 Jun 07 '18 at 11:18
  • Ah, I'd misread the significance of the designators. Yep, so this'll become a practical approach in a few years, assuming designated initialization ends up in the standard. – Sneftel Jun 07 '18 at 11:30
  • @Sneftel Compilers have started to implement it. It's generally a good sign ;) – YSC Jun 07 '18 at 11:33