bizaff
bizaff

Reputation: 189

How to match lambda to template parameter without specifying template params

I'm writing a message receiving library and want to write a simple lambda to call when a message is received on a given endpoint.

When I try a simple lambda, it doesn't match the std::function template because the lambda is not the exact type, which makes sense.

#include <iostream>
#include <unistd.h>
#include <functional>
#include <memory>

const std::string endpoint1 = "ipc:///tmp/endpoint1.ipc";
const std::string endpoint2 = "ipc:///tmp/endpoint2.ipc";

class ISubscriber {
public:
    virtual ~ISubscriber() {};
};

template <typename T>
class Subscriber : public ISubscriber
{
public:
    Subscriber(const std::string & endpoint, std::function<void (const T&)> callback);
};

class Context
{
public:
    template <typename T>
    void addSubscriberListener(const std::string & endpoint, std::function<void(const T &)> callback)
    {
        // add to subscribers list
    }

private:
    std::vector<std::unique_ptr<ISubscriber>> _subscribers;
    // All the other goo to make messaging work
};

class Type1 {};
class Type2 {};

void test()
{
    Context context;
#if 1   // Desired syntax
    context.addSubscriberListener(endpoint1, [] (const Type1 & t) {
    });
    context.addSubscriberListener(endpoint2, [] (const Type2 & t) {
    });
#else   // Undesired syntax
    context.addSubscriberListener(endpoint1, std::function<void(const Type1 &)>([] (const Type1 & t) {
    }));
    context.addSubscriberListener(endpoint2, std::function<void(const Type2 &)>([] (const Type2 & t) {
    }));
#endif 
}

The desired syntax gives me

Test.cpp: In function ‘void test()’:
Test.cpp:43:10: error: no matching function for call to ‘Context::addSubscriberListener(const string&, test()::<lambda(const Type1&)>)’
         });
          ^
Test.cpp:25:11: note: candidate: ‘template<class T> void Context::addSubscriberListener(const string&, std::function<void(const T&)>)’
      void addSubscriberListener(const std::string & endpoint, std::function<void(const T &)> callback)
           ^~~~~~~~~~~~~~~~~~~~~
Test.cpp:25:11: note:   template argument deduction/substitution failed:
Test.cpp:43:10: note:   ‘test()::<lambda(const Type1&)>’ is not derived from ‘std::function<void(const T&)>’
         });
          ^
Test.cpp:45:7: error: no matching function for call to ‘Context::addSubscriberListener(const string&, test()::<lambda(const Type2&)>)’
      });
       ^
Test.cpp:25:11: note: candidate: ‘template<class T> void Context::addSubscriberListener(const string&, std::function<void(const T&)>)’
      void addSubscriberListener(const std::string & endpoint, std::function<void(const T &)> callback)
           ^~~~~~~~~~~~~~~~~~~~~
Test.cpp:25:11: note:   template argument deduction/substitution failed:
Test.cpp:45:7: note:   ‘test()::<lambda(const Type2&)>’ is not derived from ‘std::function<void(const T&)>’
      });

It's not obvious to me if I need extra plumbing, or what that plumbing should be to make it work.

What are my options to get to the desired syntax?

Upvotes: 2

Views: 141

Answers (2)

max66
max66

Reputation: 66230

But you really need that addSubscriberListener() receive a std::function ?

What about simply accept a generic type F (for "functional") ?

template <typename F>
void addSubscriberListener (const std::string & endpoint, F callback)
{
    // add to subscribers list
}

This way you can go around your chicken-and-egg problem you have: the lambda can be converted to a std::function but isn't a std::function so the compiler must known the T type to convert the lambda to a std::function and deduct from it the T type.

Anyway... if you need to maintain the std::function... not exactly the desired syntax but... you could explicit the T type

// --------------------------VVVVVVV
context.addSubscriberListener<Type1>(endpoint1, [] (const Type1 & t) {
});
context.addSubscriberListener<Type2>(endpoint2, [] (const Type2 & t) {
});
// --------------------------^^^^^^^

It's another way to break the chicken-and-egg problem: you explicit the type T, so the compiler know that the lambda has to used to initialize a std::function<void(Type1 const &)> (fist call) and a std::function<void(Type2 const &)> (second call call).

-- Edit --

The OP precise that

at some point, I need to declare a type of the lambda parameter to unpack into, then pass that into the lambda...

The OP also precise that can use C++17 so... what about deduce T, from F, using std::function deduction guides?

Given that your callback callable receive only an argument, you could write (sorry: code not tested)

template <typename F>
void addSubscriberListener (const std::string & endpoint, F callback)
{
  using T = std::remove_cv_t<
               std::remove_reference_t<
                  typename decltype(std::function{callback})::argument_type;

  // add to subscribers list
}

Unfortuntely std::function::argument_type is available only if the std::function receive only one argument and, furthermore, is deprecated in C++17 and removed from C++20.

So maybe you could write a custom type traits as follows

template <typename>
struct getTypeFirstArg;

template <typename R, typename T1, typename ... Ts>
struct getTypeFirstArg<std::function<R(T1, Ts...)>>
 { using type = T1; };

and extract T with

   using T = std::remove_cv_t<
                std::remove_reference_t<
                   typename getTypeFirstArg<decltype(std::function{f})>::type>>;

The following is a full compiling example

#include <functional>
#include <type_traits>

template <typename>
struct getTypeFirstArg;

template <typename R, typename T1, typename ... Ts>
struct getTypeFirstArg<std::function<R(T1, Ts...)>>
 { using type = T1; };

template <typename F>
void foo (F f)
 {
   using T = std::remove_cv_t<
                std::remove_reference_t<
                   typename getTypeFirstArg<decltype(std::function{f})>::type>>;

   static_assert( std::is_same_v<T, int> );
 }

int main ()
 {
   foo([&](int const &){});
 }

See also the answer from Guillaume Racicot that should works also in C++11 and C++14.

Upvotes: 4

Guillaume Racicot
Guillaume Racicot

Reputation: 41820

You should skip the std::function and use the lambda directly:

template <typename F>
void addSubscriberListener(std::string const& endpoint, F callback)
{
    // add to subscribers list
}

This makes type deduction available for the lambda type

at some point, I need to declare a type of the lambda parameter to unpack into, then pass that into the lambda..

So if I understand you need to reflect on the parameter type of the lambda?

That can be done using a similar pattern to boost::callable:

template<typename>
struct extract_arg_types {};

template<typename R, typename C, typename Arg>
struct extract_arg_types<R(C::*)(Arg) const> {
    using arg_type = Arg;
};

Then you can use that in your class:

template <typename F>
void addSubscriberListener(std::string const& endpoint, F callback)
{
    using T = typename extract_arg_types<decltype(&T::operator())>::arg_type;
    // add to subscribers list
}

Upvotes: 1

Related Questions