Home > Software design >  Pass member function pointer to parent class yields compiler error
Pass member function pointer to parent class yields compiler error

Time:02-15

I'd like to have child classes register callbacks to their parent class so that users of the parent class can call methods of the child with a known function signature.

typedef int(*Func)(int);

class A
{
  public:
    void registerFunc(Func f)
    {}
};

class B : public A
{

  public:
    B()
    {
      A::registerFunc(&B::myF);
    }
    int myF(int x) {
       // do stuff with member variables
       return 3; 
    }
};

But I get this compiler error

main.cpp:18:23: error: cannot initialize a parameter of type 'Func' (aka 'int (*)(int)') with an rvalue of type 'int (B::*)(int)'
      A::registerFunc(&B::myF);
                      ^~~~~~~
main.cpp:8:28: note: passing argument to parameter 'f' here
    void registerFunc(Func f)

Here's a Repl illustrating the error in a concise example.

https://replit.com/@Carpetfizz/RudeSmoothComments#main.cpp

The accepted answer in a related thread suggested to override a virtual function declared in A but my use case actually requires dynamic callback registrations.

CodePudding user response:

You can try this.

typedef std::function<int (int)> Func;

class A
{
  public:
    void registerFunc(Func f)
    {}
};

class B : public A
{

  public:
    B()
    {
      A::registerFunc(std::bind(&B::myF, *this, std::placeholders::_1));
    }
    int myF(int x) {
       // do stuff with member variables
       return 3;
    }
};

CodePudding user response:

If I understand the goal (and believe me, that's a sketchy 'if'), you want to specify some member of some A derivation to invoke from some A member as a dispatched 'callback' mechanic. If that is the case, then to answer your question in comment, yes, a function and bind can do this. It can even be semi-protected with a little help from sfinae:

Example

#include <iostream>
#include <type_traits>
#include <functional>
#include <memory>

struct A
{
    virtual ~A() = default;

    std::function<void(int)> callback = [](int){};

    template<class Derived>
    std::enable_if_t<std::is_base_of<A, Derived>::value>
    registerCallback(void (Derived::*pfn)(int))
    {
        using namespace std::placeholders;
        callback = std::bind(pfn, dynamic_cast<Derived*>(this), _1);
    }

    void fire(int arg)
    {
        callback(arg);
    }
};

struct B : public A
{
    void memberfn(int arg)
    {
        std::cout << __PRETTY_FUNCTION__ << ':' << arg << '\n';
    }
};

struct Foo
{
    void memberfn(int arg)
    {
        std::cout << __PRETTY_FUNCTION__ << ':' << arg << '\n';
    }
};

int main()
{
    std::unique_ptr<A> ptr = std::make_unique<B>();
    ptr->registerCallback(&B::memberfn);
    // ptr->registerCallback(&Foo::memberfn); // WILL NOT WORK
    ptr->fire(42);
}

Output

void B::memberfn(int):42

The Parts

The first part is straight forward. We declare a member variable callback to be a std::function<void(int)> instance. This is where we'll eventually bind our callable object point. The default value is a lambda that does nothing.


The second part is... a little more complicated:

template<class Derived>
std::enable_if_t<std::is_base_of<A, Derived>::value>
registerCallback(void (Derived::*pfn)(int))

This declares registerCallback as an available member function that accepts a non-static member function pointer taking one int as an argument, but only if the class hosting that member function, or a derivative therein, is a derivation of A (or A itself). Some non-A derivative Foo with a member void foo(int) will not compile.


Next, the setup to the callback itself.

using namespace std::placeholders;
callback = std::bind(pfn, dymamic_cast<Derived*>(this), _1);

This just binds the pointer-to-member to this dynamic-cast to the derivation type (which had better work or we're in trouble, see final warning at the end of this diatribe), and sets the call-time placeholder. The _1 you see comes from the std::placeholders namespace, and is used to delay providing an argument to the callback until such time as we actually invoke it (where it will be required,and you'll see that later). See std::placehholders for more information.


Finally, the fire member, which does this:

void fire(int arg)
{
    callback(arg);
}

This invokes the registered function object with the provided argument. Both the member function and this are already wired into the object. The argument arg is used to fill in the placeholder we mentioned earlier.


The test driver for this is straightforward:

int main()
{
    std::unique_ptr<A> ptr = std::make_unique<B>();
    ptr->registerCallback(&B::memberfn);
    // ptr->registerCallback(&Foo::memberfn); // WILL NOT WORK
    ptr->fire(42);
}

This creates a new B, hosting it in a dynamic A pointer (so you know there is no funny business going on). Even with that, because B derived from A the registerCallback sfinae filtering passes inspection and the callback is registered successfully. We then invoke the fire method, passing our int argument 42, which will be sent to the callback, etc.


Warning: With great power comes great responsibility

Even those there is protection from passing non-A derived member functions, there is absolutely none from the casting itself. It would be trivial to craft a basic A, pass a B member (which will work since A is its base), but there is no B actually present.

You can catch this at runtime via that dynamic_cast, which we're currently not error checking. For example:

registerCallback(void (Derived::*pfn)(int))
{
    using namespace std::placeholders;
    Derived *p = dynamic_cast<Derived*>(this);
    if (p)
        callback = std::bind(pfn, p, _1);
}

You can choose the road more risky. Personally, i'd detect the null case and throw an exception just to be safe(er)

  • Related