Loki Astari
Loki Astari

Reputation: 264331

Lambda callback depends on its parameters

I am writing a library where the user provides a callback as a lambda. In the default scenario I want to just call the lambda and pass the back an object.

Now there are non trivial senarios where the user may want the context as well. So I want to be able to use the same callback mechanism and just allow the user to add a context as a parameter to their lambda and I will then pass both the object and the context.

I can't quite get SFINAE to work.

I have simplified the code to this:

#include <string>
#include <iostream>

class Context {};


template<typename F>
struct UseContext
{
    // I want to set this value to 0 or 1 based on the parameters
    // in F but can't quite get this to work.
    enum {value = 0 };
};


template<typename F, typename T, bool useContext = UseContext<F>::value>
struct Caller;

template<typename F, typename T>
struct Caller<F, T, true>
{
    void operator()(F& func, Context& context, T& object)
    {
        func(context, object);
    }
};
template<typename F, typename T>
struct Caller<F, T, false>
{
    void operator()(F& func, Context&, T& object)
    {
        func(object);
    }
};



template<typename T, typename F>
void doWork(F&& func)
{
  Context  context;
  T        object;

  /// STUFF
  Caller<F,T>  caller;
  caller(func, context, object);
}

Usage:

int main()
{
    // if UseContext::value == 0 then this compiles.
    // This is the normal situation.
    doWork<std::string>([](std::string const& x){ std::cout << x << "\n";});

    // if UseContext::value == 1 then this compiles.
    // This is if the user wants more context about the work.
    // most of the time this extra parameter is not required.
    // So I don't want to force the user to add it to the parameter
    // list of the lambda.
    doWork<std::string>([](Context&, std::string const& x){ std::cout << x << "\n";});
}

Or if there is a better way of doing this.

Upvotes: 2

Views: 478

Answers (2)

T.C.
T.C.

Reputation: 137311

Expression SFINAE:

template<class F, class T>
auto call(F& func, Context& context, T& object) -> decltype(func(context, object), void())
{
   func(context, object);
}
template<class F, class T>
auto call(F& func, Context&, T& object) -> decltype(func(object), void())
{
    func(object);
}

Then just call(func, context, object). This is ambiguous if both forms are valid. If you want to disambiguate, just add a dummy parameter and do the usual int/long trick.

Upvotes: 5

David Haim
David Haim

Reputation: 26476

My solution is to use std::is_constructible plus std::enable_if:

template<typename F,typename T>
typename std::enable_if<std::is_constructible<std::function<void(T const&)>,F>::value>:type doWork(F func)
{
//...
}

template<typename F,typename T>
typename std::enable_if<std::is_constructible<std::function<void(Context&,T const&)>,F>::value>:type doWork(F func)
{
//...
}

explenation - each std::function can be built from the equivilant lambda. here we are testing using std::enable_if if you can build a std::function<void(T)> or a std::function<void(Context,T)> and re-wire the correct function in compile time.

Upvotes: 1

Related Questions