austinbv
austinbv

Reputation: 9491

C++/Arduino Pass a function as argument

I am trying to create a callback style API and am fairly new to C++. I keep getting the error error: invalid use of non-static member function but am not sure the next steps. I would like to have the ability to pass an member function as an argument to another class.

The code is something like

class Button {
  int buttonDownTime = 0;
  int debounceTime = 2000;

  ...

  template<typename Callback>
  void debounce(Callback func) {
    if (millis() - buttonDownTime > debounceTime) {
      func();
    }
  }
}

class Player {
  int playerCount = 0;

  void incriment() {
    playerCount++;
  }
}

void loop() {

  ...

  button.debounce(player.incriment);
}

EDIT:

So I want to thank everyone for the awesome answers so far but I learned something since posting. Arduino's AVR does not include C++'s <functional>. Is this a possible thing to do without that library?

Thank you again!

Upvotes: 9

Views: 5179

Answers (3)

Miles Budnek
Miles Budnek

Reputation: 30579

Non-static member functions require an object to work on, and thus can't be passed and called like normal function pointers.

The simplest way to make your debounce method work, would be to use a lambda that captures your player object and calls increment on it:

class Button {
  //...
  template<typename Callback>
  void debounce(Callback&& func) {  // <<-- Note the && here, without
                                    //      it func would need to be
                                    //      copied
    if (millis() - buttonDownTime > debounceTime) {
      func();
    }
  }
}

void loop() {
  //...
  button.debounce([&player](){ player.incriment(); });
}

With a little bit of extra effort, you could implement something similar to C++17's std::invoke to uniformly invoke any type of callable. Since you're on Arduino and don't have access to the C++ standard library, you'll need to implement std::remove_reference and std::forward yourself as well:

template <typename T>
struct remove_reference
{
  using type = T;
};

template <typename T>
struct remove_reference<T&>
{
  using type = T;
};

template <typename T>
struct remove_reference<T&&>
{
  using type = T;
};

template <typename T>
constexpr T&& forward(typename remove_reference<T>::type& t)
{
  return static_cast<T&&>(t);
}

template <typename T>
constexpr T&& forward(typename remove_reference<T>::type&& t)
{
  return static_cast<T&&>(t);
}

template <typename Callable, typename... Args>
auto invoke(Callable&& func, Args&&... args) 
    -> decltype(forward<Callable>(func)(forward<Args>(args)...))
{
    return forward<Callable>(func)(forward<Args>(args)...);
}

template <typename Callable, typename Class, typename... Args>
auto invoke(Callable&& method, Class&& obj, Args&&... args)
    -> decltype((forward<Class>(obj).*method)(forward<Args>(args)...))
{
    return (forward<Class>(obj).*method)(forward<Args>(args)...);
}

class Button {
  //...
  template<typename Callback, typename... Args>
  void debounce(Callback&& func, Args&&... args) {
    if (millis() - buttonDownTime > debounceTime) {
      invoke(forward<Callback>(func),
             forward<Args>(args)...);
    }
  }
}

void loop() {
  //...
  button.debounce(&Player::increment, player);
}

This doesn't quite do everything that C++17's std::invoke does, but it's close enough to implement a basic callback. It also gives you extra flexibility in that you could pass additional arguments to debounce and they will be passed along to your callback:

void foo(int num) { /*...*/ }

void loop() {
    Button b;
    b.debounce(foo, 42);
}

This doesn't really work if you need to save the callback and call it later, but it doesn't look like that's what you're trying to do.

Upvotes: 4

Didac Perez Parera
Didac Perez Parera

Reputation: 3834

Another valid approach is to define an interface with the pure virtual func() method so that you can pass an implementation of this interface to your debounce() method and call its func() method there.

class YourInterface {
public:
  virtual void func() = 0;
};

class Button {
public:
  int buttonDownTime = 0;
  int debounceTime = 2000;

  ...

  void debounce(YourInterface yourInterface) {
    if (millis() - buttonDownTime > debounceTime) {
      yourInterface->func();
    }
  }
}

class Player {
public:
  int playerCount = 0;

  void increment() {
    playerCount++;
  }
}

void loop() {

  ...

  button.debounce(player.increment);
}

Upvotes: 1

Peter A
Peter A

Reputation: 21

In C++, the class name (Player) would be part of function signature, and the syntax becomes non-trivial. Templates do not resolve it in an elegant way, and even what Boost gives you (look here), is not really elegant (in my book).

Here is a simpler syntax which achieves the same goal.

class Button {
  int buttonDownTime = 0;
  int debounceTime = 2000;

  public:
  template<typename CallbackClass>
  void debounce(CallbackClass* po) {
    if (millis() - buttonDownTime > debounceTime) {
      po->increment();
    }
  }
};

class Player {
  int playerCount = 0;

  public:
  void increment() {
    playerCount++;
  }
};

int main() {
  Button button;
  Player player;
  button.debounce(&player);
}

Upvotes: 2

Related Questions