4

I'd like to be able to enforce a particular callback signature when using a templated callback parameter. The goals are three-fold:

  1. Get a compiler error at the user call site, not deep inside the method accepting the callback.
  2. Obey the least surprise principle and prevent the user from providing a callback that returns a value when the implementation using the callback will not check its value etc (exact signature match)
  3. Use idiomatic c++ and best practices as much as possible. Be efficient.

Coming from C#, I first attempted to mimic the Action and Func classes like so:

template<typename ...TArgs> using Action = std::function<void(TArgs...)>;
template<typename TRet, typename ...TArgs> using Func = std::function<TRet(TArgs...)>;

This works ok, except it uses std::function which I've heard to avoid for this 'simple' task (fails #3). Also, unfortunately, it fails my #2 requirement above as well:

void DoActionStuff(Action<int, float> callback)
{
    callback(1, 2.0);
}

void DoFuncStuff(Func<float, int, float> callback)
{
    float result = callback(1, 2.0);
    if (result > 100.0) { throw runtime_error("derp"); };
}

DoActionStuff([](int x, float y) { cout << x << " " << y << endl; }); // OK
DoActionStuff([](int x, float y) { cout << x << " " << y << endl; return x * y; }); // No error :( Why not?

DoFuncStuff([](int x, float y) { cout << x << " " << y << endl; }); // OK - We get a compiler error right here at the call-site for this one...
DoFuncStuff([](int x, float y) { cout << x << " " << y << endl; return x * y; }); // OK

How do I meet all 3 requirements doing something like the following? Is there any, nice, trait magic I could use to provide a better signature for this method to convey exactly what type of callback is required?

template<typename TCallback>
void DoStuff(TCallback callback) { ... } // Unnecessarily burdens the client of this api (they have no idea the signature to use here)
Roger B
  • 109
  • 3
  • 1
    And what if I want to do `DoActionStuff([](auto i, auto f){ cout << i+f; })`? You want to disallow that? – Barry Sep 22 '16 at 06:57

5 Answers5

1

As long as you use non-capturing lambdas, you can rely on the fact that they decay to function pointers:

void doActionStuff(void(*callback)(int, double)) { callback(0, 0.); }
int main() { doActionStuff([](int, double) { }); }

Otherwise, if you want enforce the check and still use a capturing lambda, you can put a static_assert in your function to verify the return type and the prototype of the function F:

#include <type_traits>
#include <utility>

template<typename F>
int doActionStuff(F &&f) {
    static_assert(std::is_same<std::result_of_t<F(int, double)>, int>::value, "!");
    return std::forward<F>(f)(0, 0.);
}

int main() {
    int i = 0;
    doActionStuff([](int, double) { return 42; });
    doActionStuff([i](int, double) { return 42; });
    // these won't work
    // doActionStuff([i](int) { return 42; });
    // doActionStuff([](int, double) { });
}
skypjack
  • 49,335
  • 19
  • 95
  • 187
  • 1
    Good call on the capturing vs. non-capturing. I hadn't even thought that that would alter the signature. Now that I know, I of course want it :) So that potentially makes the function pointer option even less appealing. Really wish C++ had a nice way to express this... – Roger B Sep 22 '16 at 08:37
1

Honestly, I would stick with your first instinct, less the type aliases:

void DoAction(std::function<void(int, float)> );
void DoFuncStuff(std::function<float(int, float)> );

It is clearly visible at the call site what these functions expect, they already do validity checking for you. They won't check exactly the signature - but is that really what you want? Any return value for callables to DoAction() will get ignored, implicit conversions will be allowed for the argument types. But that's always true in C++ everywhere. Moreover, this allows more complicated constructions like passing in generic lambdas:

DoFuncStuff([](auto i, auto f){ return f; }); // really want to disallow this?

You could of course write something like:

// function pointer case
template <class R, class... Args> std::true_type check_member(R (*)(Args...) );

// function object case
template <class F, class R, class... Args> std::true_type check_member(R (F::*)(Args...) );
template <class F, class R, class... Args> std::true_type check_member(R (F::*)(Args...) const );
template <class R, class... Args, class F> auto check_member(F& ) -> decltype(check_member<F, R, Args...>(&F::operator()));

// failure case
template <class... > std::false_type check_member(...);

template <class F>
void DoAction(F f) {
    static_assert(
        decltype(check_member<void, int, float>(f))::value,
        "invalid signature"
    );

    // body
}

But that's way less clear than your original idea. And you need the check_member stuff to correctly handle overloaded and templated operator().


Modulo certain exceptions, per usual.

Barry
  • 286,269
  • 29
  • 621
  • 977
  • Hmm, yeah, maybe I'll just stay with std::function then. It's a large 64byte structure but stack is cheap right :) So far as I've been exploring traits I've steered clear of static_assert - I don't want my phrasing and speelings to get in the way of nicer compiler output that it should be able to provide (I'd have to repeat myself to put the signature in the output message to be nice). Yeah, I guess I forgot about 'auto' as well so I guess I'll have to allow it to be generic there; still sad about allowing return values in certain instances though but I suppose that's just the way it is. – Roger B Sep 22 '16 at 08:28
  • @Roger Your own message in a `static_assert` will be much clearer than what a compiler could produce. – Barry Sep 22 '16 at 08:34
  • @RogerB Also note in that whole mess of template code I'm still disallowing signatures like `void(int const&, float const&)`, which just seems... wrong. – Barry Sep 22 '16 at 08:39
1

std::function is reasonably efficient. It is two pointer indirections on call, and only allocates if the object being passed is large in most quality C++ standard library implementations. (MSVC2015 won't allocate if it is as large as two std::strings, for example).

I would only shy at std::function if I was going to call it in a per-pixel per-frame render loop, or similar extremely time-sensitive situation. Even per-scanline it would be acceptable.

So std::function solves #1 and #3.

As for #2, that is questionable. Most implicit conversions are what you want to do. Your one possible case is that you don't want return values to be ignored.

std::function<void()> used to complain when passed a function returning something, but that was explicitly and on purpose changed in the standard to work and discard the return value. Idiomatically, int() signature can be pased to void() and everything is acceptable.

You don't want that, and that is a reasonable position.

If I really needed that, I'd wrap std::function up.

template<class Sig>
struct func:std::function<Sig>{
  using std::function<Sig>::function;
};
template<class...Args>
struct func<void(Args...)>:std::function<void(Args...)> {
  using Base=std::function<void(Args...)>;

  func()=default;
  func( func const& )=default;
  func( func && )=default;
  func& operator=( func const& )=default;
  func& operator=( func && )=default;

  template<class Fin,
    std::enable_if_t<!std::is_same<std::decay_t<Fin>, func>{}, int> =0,
    std::enable_if_t<std::is_same<std::result_of_t<Fin&(Args...)>, void>{}, int> =0
  >
  func( Fin&& fin ):Base(std::forward<Fin>(fin)) {}

  template<class...Fs,
    std::enable_if_t<(std::sizeof...(Fs)>1), int> =0
  >
  func( Fs&&... fs ):Base(std::forward<Fs>(fs)...) {}

  template<class Fin,
    std::enable_if_t<!std::is_same<std::decay_t<Fin>, func>{}, int> =0,
    std::enable_if_t<std::is_same<std::result_of_t<Fin&(Args...)>, void>{}, int> =0
  >
  func& operator=( Fin&& fin ) {
    Base::operator=(std::forward<Fin>(fin));
    return *this;
  }
};

but I'd just accept that spurious return values can be ignored.

Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524
  • Indeed, implicit conversions are desired. Ignoring return values is not. Additionally for perf, the call to the std::function is apparently never inlinable whereas a straight template solution may very often well be. There's tradeoffs at every turn it seems... – Roger B Sep 22 '16 at 19:54
  • @RogerB almost never inlinable. gcc and clang now inline `std::function`s in some cases. You can use constrained templates. Again, note that your desire to not discard return values violates standard C++ idioms, where I can have redundant return values and expect them to be evaluated in a `void` context. – Yakk - Adam Nevraumont Sep 22 '16 at 20:25
0

You may use SFINAE:

template <typename F>
auto DoActionStuff(F&& f) -> decltype(std::declval<int&>() = f(42, 4.2f), void());
Jarod42
  • 203,559
  • 14
  • 181
  • 302
-1

You may roll back to function pointers instead of std::function. For example with:

template<typename ...TArgs>
using actionPtr = void (*)(TArgs...);

and

void DoActionStuff(actionPtr<int, float> callback)
{
    callback(1, 2.0);
}

instead of your Action, we'll get:

DoActionStuff([](int x, float y) {
    cout << x << " " << y << endl;
}); // OK
DoActionStuff([](int x, float y) {
    cout << x << " " << y << endl; return x * y;
}); // Woohoo! Now we get error here!

And an analogical declaration for Func:

template<typename TRet, typename ...TArgs>
using funcPtr = TRet (*)(TArgs...);

It meets your (1), (2) and, let's say, (2.5) requirements, as your claim for(3) is somewhat controversial. In modern C++ std::function seems to be more idiomatic than function pointers, as it allows some special features with conjunction to std::bind and some other functional stuff. Also it's more general, as you can convert (implicitly, if you want) a function pointer to std::function, but not vice-versa.

Community
  • 1
  • 1
Sergey
  • 7,985
  • 4
  • 48
  • 80
  • @Downvoter, please explain what's wrong with my answer? – Sergey Sep 22 '16 at 07:24
  • yes std::function has some nice benefits (easier interop with member functions inside a class etc.) My quip about performance is that std::function is 'big' (64 bytes on the stack in my implementation to handle small functions supposedly). I may end up sticking with std::function after all... trying to stay away from pointers in general as they utterly gum up the function signature but I'll keep this in mind as I go deeper... – Roger B Sep 22 '16 at 08:25