rootedb
rootedb

Reputation: 21

Syntax for overloaded template functions with lambda function arguments

I need help with the syntax for the following C++ (17) code:

#include <iostream>

struct st_a {
  int var;
};

struct st_b {
  double var;
};


void func(void(cb)(st_a)) {
  st_a a;
  a.var = 3;
  cb(a);
}

void func(void(cb)(st_b)) {
  st_b b;
  b.var = 4;
  cb(b);
}

class test {
public:
  test() {

    func([](st_a arg) { std::cout << arg.var; });
    
    func([this](st_a arg) { my_func_a(arg); });

    func([this](st_b arg) { my_func_b(arg); });

  }

  void my_func_a(st_a arg) {
    std::cout << arg.var;
  }

  void my_func_b(st_b arg) {
    std::cout << arg.var;
  }
};



int main(void)
{
  test t;
}

  

Unsurprisingly the compiler gives me

error: no matching function for call to ‘func(test::test()::<lambda(st_a)>)’

I attempted to rectify it using template functions, i.e.,

template<typename F>
void func(F cb) {
  st_a a;
  a.var = 3;
  cb(a);
}

template<typename F>
void func(F cb) {
  st_b b;
  b.var = 4;
  cb(b);
}

But this results in

error: redefinition of ‘template<class F> void func(F)’

...which is no surprise.

My question is how can I change the template parameters to specify the function argument?

Follow up from analyzing replies

I forgot to mention that I'm chasing nanoseconds, so std::function is not an option.

The assembly output from Yksisarvinen's and max66's solutions is identical, branch-free, and very compact (the whole program compiles into 9 assembly instructions). The std::function assembly on the other hand contains page upon page of branches and function calls, which invalidates this approach from an execution speed prespective.

From a code readability perspective, perhaps std::function is the best option. In my opinion, the second best alternative is the one max66 presented, i.e

template <typename F> auto func (F cb) -> decltype(cb(std::declval<st_a>()), void()) { ...; }

this is also the best approach for portability (C++11) when compared to Yksisarvinen's solution.

Thank you for your help.

Upvotes: 2

Views: 132

Answers (4)

max66
max66

Reputation: 66200

Another way (almost equivalent to the std::is_invocable_v answer from Yksisarvinen, but working also before C++17) could be the following

template <typename F>
auto func (F cb) -> decltype( cb(std::declval<st_a>()), void() )
 {
   st_a a{3};
   cb(a);
 }

template <typename F>
auto func (F cb) -> decltype( cb(std::declval<st_b>()), void() )
 {
   st_b b{4};
   cb(b);
 }

Upvotes: 1

Yksisarvinen
Yksisarvinen

Reputation: 22219

The other answers explain the core issue with capturing lambda, but you can solve the problem without std:function by using some template metaprogramming with std::enable_if.

template<typename F, std::enable_if_t<std::is_invocable_v<F, st_a>, bool> = true>
void func(F cb) {
  st_a a;
  a.var = 3;
  cb(a);
}

template<typename F, std::enable_if_t<std::is_invocable_v<F, st_b>, bool> = true>
void func(F cb) {
  st_b b;
  b.var = 4;
  cb(b);
}

With definitions as such, compiler will only generate first func if F can be invoked with argument of type st_a and second func if the argument can be invoked with argument of type st_b. It will probably break if both could be true at the same time (e.g. if you introduce inheritance between st_a and st_b).

See it online

Upvotes: 2

asynts
asynts

Reputation: 2423

The problem is that the lambdas defined here

func([this](st_a arg) { my_func_a(arg); });
func([this](st_b arg) { my_func_b(arg); });

have captures. You can think of a lambda as a class that implements operator(). Lambdas with captures have members in that class.

If you have a lambda with no captures, the compiler provides an implicit conversion to a function pointer, because this is essentially what it is.

If you have a lambda with captures, the compiler can not provide an implicit conversion because you would not be able to fit all these captures into a single pointer.

(If you are interested in lambdas in C++, I'd recommend looking at Jason Turners "Weekly C++" episodes from which many are about lambdas. This one talks about what lambdas are.)

The C++ standard library provides std::function which is some type-erasure magic that, honestly, I don't fully understand. (I remember watching a C++ Weekly episode about that, but I can't recall properly what it does.)

Anyways, by taking a std::function you get a small overhead and the standard library will just make everything work.

Upvotes: 1

Dario Petrillo
Dario Petrillo

Reputation: 1113

You are passing a lambda function to something that expects a function pointer, so the type doesn't match. I would suggest changing func to accept a std::function as callback, so that you can pass a lambda to it

#include <functional>

void func(std::function<void(st_a)> cb) {
  st_a a;
  a.var = 3;
  cb(a);
}

void func(std::function<void(st_b)> cb) {
  st_b b;
  b.var = 4;
  cb(b);
}

Upvotes: 0

Related Questions