Steven Welch
Steven Welch

Reputation: 63

How do I pass an instance of a object function to another function?

I have a class that is trying to encapsulate the setup of an interrupt. I need to pass an instantiated reference/pointer/etc to an external function that then calls my function.

I can't change this function:

typedef void (*voidFuncPtr)(void);
void attachInterrupt(uint32_t pin, voidFuncPtr callback, uint32_t mode);

My Library:


class Buttons ... 

bool Buttons::begin(int buttonPin)
{
    //attachInterrupt(buttonPin, (this->*func)Buttons::released, HIGH);
    attachInterrupt(buttonPin, &Buttons::released, LOW);

return 0;
}

void Buttons::released()
{
    numButtonPresses++;
    pressLength = millis()-pressStartTime;
    pressStartTime = 0;
    buttonState=LOW;
}

The problem is that I don't know what to put inside the attachInterupt function in the Buttons::begin method. I am sure I am doing something wrong in the way I am approaching this problem, but I am not sure what it is or what I should do about it. I would greatly appreciate any suggestions!

Upvotes: 3

Views: 586

Answers (3)

Davislor
Davislor

Reputation: 15164

The problem you were facing is that you wanted to pass two pieces of data as your callback: the member function, and a class instance to call that member function on.

With the existing interface, which only accepts a function pointer with no arguments, you might create a static Button object and then write a static wrapper function that calls someStaticButton.released(). Then you could pass the address of this wrapper function as your callback. Justin Time’s answer approaches from a different, and clever, direction: wrap the instance and member in a singleton class, and give that a static member callback function. A simpler way to allow more than one singleton class would be to add a numeric ID as template parameter.

You might also be able to make button::released() a static member function, but your implementation appears to refer to member data. This wouldn’t be an option unless there is only one button in the program, represented by a singleton class.

If you can pass the instance pointer as the first argument to your callback function, or any other argument that can do a round-trip conversion to and from an object pointer (such as void* or any integer as wide as intptr_t), you can make the member function static and pass the this pointer as its argument.

If you can overload attachInterrupt to take a std::function object as your callback, you can do what you originally wanted. This type can represent a static function, a member function, or a closure containing a member function and a this pointer.

You unfortunately say you cannot change the callback function, but perhaps you can extend the interface in a backward-compatible way, such as:

#include <array>
#include <iostream>
#include <functional>
#include <stdint.h>
#include <stdlib.h>
#include <utility>

using std::cout;
using std::endl;
using voidFuncPtr = void(*)(void);
using Callback = std::function<void(void)>;

std::array<Callback, 1> interrupts;

void attachInterruptEx( const uint32_t pin,
                        Callback&& callback,
                        const uint32_t /* unused */ )
{
  interrupts.at(pin) = std::move(callback);
}

void attachInterrupt( const uint32_t pin,
                      const voidFuncPtr callbackPtr,
                      const uint32_t mode )
{
  /* Passing callbackPtr to a function that expects Callback&& implicitly
   * invokes the constructor Callback(voidFuncPtr).  This is sugar for
   * std::function<void(void)>(void(*)(void)). That is, it initializes a
   * temporary Callback object from callbackPtr.
   */
  return attachInterruptEx( pin, callbackPtr, mode );
}

class Buttons {
  public:
     Buttons() = default;

     bool begin(int buttonPin);
     void released();
     unsigned long buttonPresses() { return numButtonPresses; }

  private:
     // Empty stub function, probably calls a timer.
     unsigned long millis() { return 0; };

     static constexpr uint32_t LOW = 0;
     uint32_t buttonState = LOW;
     unsigned long numButtonPresses = 0;
     unsigned long pressStartTime = 0;
     unsigned long pressLength = 0;
};

/* Per C++17, a constant known at compile time is not odr-used, so this
 * definition is deprecated.  Still included out of an abundance of caution.
 * It cannot have an initializer.
 */
constexpr uint32_t Buttons::LOW;

bool Buttons::begin(int buttonPin)
{
/* The C++17 Standard says little about the return type of std::bind().
 * Since the result is a callable object, a std::function can be initialized
 * from it.  I make the constructor call explicit in case the return type of
 * std::bind is a subclass of std::function in some implementation, and
 * it resolves the overload in a way we don't expect.
 */
  attachInterruptEx( static_cast<uint32_t>(buttonPin),
                     Callback(std::bind(&Buttons::released, this)),
                     LOW );

  return false;
}

void Buttons::released()
{
    numButtonPresses++;
    pressLength = millis()-pressStartTime;
    pressStartTime = 0;
    buttonState=LOW;
}

int main(void)
{
  Buttons buttons;

  buttons.begin(0);
  interrupts[0]();

  // Should be 1.
  cout << buttons.buttonPresses() << endl;

  return EXIT_SUCCESS;
}

Upvotes: 2

[Note that this code will use the following simplified version of your MCVE, primarily to have an easily-usable callback caller when testing & demonstrating the implementation:]

// Pointer type.
typedef void (*voidFuncPtr)(void);

// Dummy callback callers.
void takesVoidFuncPtr(voidFuncPtr vfp) {
    std::cout << "Now calling vfp...\n";
    vfp();
    std::cout << "vfp called.\n";
}
struct DelayedCaller {
    voidFuncPtr ptr;
    DelayedCaller(voidFuncPtr p) : ptr(p) {}
    void callIt() { return ptr(); }
};

// Simple stand-in for Button.
struct HasMemberFunction {
    std::string name;

    HasMemberFunction(std::string n) : name(std::move(n)) {}

    void memfunc() { std::cout << "-->Inside " << name << ".memfunc()<--\n"; }
    void funcmem() { static std::string out("Don't call me, I'm lazy. >.<\n"); std::cout << out; }
};

// Calling instance.
HasMemberFunction hmf("instance");

Ideally, you'd be able to bind the function to an instance with a lambda, and pass that to the consuming function as the callback. Unfortunately, though, capturing lambdas can't be converted to non-member function pointers, so this option isn't viable. However...

[Note that I have omitted proper encapsulation both for brevity, and for a demonstration of this approach's caveats at the end of the answer. I would recommend adding it if you actually use this.]

// Helper.
// Can easily be simplified if desired, Caller and MultiCaller only use FuncPtrTraits::ContainingClass.
namespace detail {
    template<typename...> struct Pack {};

    template<typename T> struct FuncPtrTraits;

    template<typename Ret, typename Class, typename... Params>
    struct FuncPtrTraits<Ret (Class::*)(Params...)> {
        using ReturnType = Ret;
        using ContainingClass = Class;
        using ParameterList = Pack<Params...>;
    };

    template<typename T>
    using ContainingClass = typename FuncPtrTraits<T>::ContainingClass;
} // detail

// Calling wrapper.
// Only allows one pointer-to-member-function to be wrapped per class.
template<typename MemFunc, typename Class = detail::ContainingClass<MemFunc>>
struct Caller {
    static_assert(std::is_member_function_pointer<MemFunc>::value, "Must be built with pointer-to-member-function.");

    static Class*   c;
    static MemFunc mf;

    static void prep(Class& cls, MemFunc mem) { c = &cls; mf = mem; }
    static void clean() { c = nullptr; mf = nullptr; }
    static void call() { return (c->*mf)(); }

    // Constructor is provided for convenience of creation, to effectively tie prep() to deduction guide.
    // Note that it operates on static members only.
    // Convenient, but likely confusing.  ...Probably best not to do this. ;3
    Caller(Class& cls, MemFunc mem) { prep(cls, mem); }

    // Default constructor is required if we provide the above hax ctor.
    Caller() = default;
};
template<typename MemFunc, typename Class> Class*  Caller<MemFunc, Class>::c  = nullptr;
template<typename MemFunc, typename Class> MemFunc Caller<MemFunc, Class>::mf = nullptr;

We can instead provide the desired behaviour by using a wrapper class, which stores the desired function and instance as static member variables, and provides a static member function that matches voidFuncPtr and contains the actual, desired function call. It can be used like so:

std::cout << "\nSimple caller, by type alias:\n";
using MyCaller = Caller<decltype(&HasMemberFunction::memfunc)>;
MyCaller::prep(hmf, &HasMemberFunction::memfunc);
takesVoidFuncPtr(&MyCaller::call);
MyCaller::clean(); // Not strictly necessary, but may be useful once the callback will no longer be called.

// Or...

std::cout << "\nSimple caller, via dummy instance:\n";
Caller<decltype(&HasMemberFunction::memfunc)> caller;
caller.prep(hmf, &HasMemberFunction::memfunc);
takesVoidFuncPtr(&caller.call);
caller.clean(); // Not strictly necessary, but may be useful once the callback will no longer be called.

That's a bit of a mess, so a hacky constructor is provided to simplify it.
[Note that this may violate the principle of least astonishment, and thus isn't necessarily the best option.]

std::cout << "\nSimple caller, via hax ctor:\n";
Caller clr(hmf, &HasMemberFunction::memfunc);
takesVoidFuncPtr(&clr.call);
clr.clean(); // Not strictly necessary, but may be useful once the callback will no longer be called.

Now, this version only allows one function to be wrapped per class. If multiple functions per class are required, we'll need to expand it a little.

[Note that this would be pretty awkward to typedef, as the optimal template parameter order places the first pointer-to-member-function first, to allow Class to be automatically deduced if only one function needs to be wrapped; the "hax ctor" approach was primarily intended for this version of Caller, although a deduction guide could likely also be used to swap Class and MemFunc.]
[Also note that all wrapped member functions must have the same signature.]

// Calling wrapper.
// Slightly more complex version, allows multiple instances of MemFunc to be wrapped.
template<typename MemFunc, typename Class = detail::ContainingClass<MemFunc>, typename... MemFuncs>
struct MultiCaller {
    static_assert(std::is_member_function_pointer<MemFunc>::value, "Must be built with pointer-to-member-function.");
    static_assert(std::conjunction_v<std::is_same<MemFunc, MemFuncs>...>, "All pointers-to-member-function must be the same type.");

    static Class* c;
    static std::array<MemFunc, 1 + sizeof...(MemFuncs)> mfs;

    static void prep(Class& cls, MemFunc mem, MemFuncs... mems) { c = &cls; mfs = { mem, mems... }; }
    static void clean() { c = nullptr; for (auto& m : mfs) { m = nullptr; } }

    // Registered functions are wrapped by index, with index specified as a template parameter to match empty parameter list.
    template<size_t N = 0, bool B = (N < mfs.size())>
    static void call() {
        static_assert(B, "Index must be a valid index for mfs.");
        return (c->*mfs[N])();
    }

    // Constructor is provided for convenience of creation, to effectively tie prep() to deduction guide.
    // Note that it operates on static members only.
    // Convenient, but likely confusing.  Primarily used because instantiation & preparation get really repetitive otherwise.
    MultiCaller(Class& cls, MemFunc mem, MemFuncs... mems) { prep(cls, mem, mems...); }

    // Default constructor is required if we provide the above hax ctor.
    MultiCaller() = default;
};
template<typename MemFunc, typename Class, typename... MemFuncs> Class*                                       MultiCaller<MemFunc, Class, MemFuncs...>::c   = nullptr;
template<typename MemFunc, typename Class, typename... MemFuncs> std::array<MemFunc, 1 + sizeof...(MemFuncs)> MultiCaller<MemFunc, Class, MemFuncs...>::mfs = {nullptr};

It can be used by type alias as before, or the "hax ctor" can be used to couple it to a deduction guide like so:

std::cout << "\nMulti-registration caller, by type alias:\n";
using MyMultiCaller = MultiCaller<decltype(&HasMemberFunction::memfunc), HasMemberFunction, decltype(&HasMemberFunction::funcmem)>;
MyMultiCaller::prep(hmf, &HasMemberFunction::memfunc, &HasMemberFunction::funcmem);
takesVoidFuncPtr(&MyMultiCaller::call<0>); // memfunc
takesVoidFuncPtr(&MyMultiCaller::call<1>); // funcmem
MyMultiCaller::clean(); // Not strictly necessary, but may be useful once the callback will no longer be called.

// Or...

std::cout << "\nMulti-registration caller, via hax ctor:\n";
MultiCaller mclr(hmf, &HasMemberFunction::memfunc, &HasMemberFunction::funcmem);
takesVoidFuncPtr(&mclr.call<0>); // memfunc
takesVoidFuncPtr(&mclr.call<1>); // funcmem
mclr.clean(); // Not strictly necessary, but may be useful once the callback will no longer be called.

Note that in all cases, this has all the caveats of static members. In particular, since the binding relies on the static class members and isn't contained within the wrapper function itself, modifying the members after a callback is passed will immediately change the results of calling said already-passed callback.

std::cout << "\nBut alas:\n";
MyCaller::prep(hmf, &HasMemberFunction::memfunc);
DelayedCaller dc(&MyCaller::call);
dc.callIt(); // Output: "-->Inside instance.memfunc()<--"
std::cout << "Changing the registered instance will...\n";
HasMemberFunction hmf2("spinstance");
MyCaller::c = &hmf2;
dc.callIt(); // Output: "-->Inside spinstance.memfunc()<--"

You can see the different variants here, on Compiler Explorer.

Upvotes: 1

Machinegon
Machinegon

Reputation: 1885

You're using an old c-style function pointer callback. This does not work for member function of an object. If you can't change the callback, you need to make the Buttons::released function static.

Upvotes: 5

Related Questions