How to work around C++ pointer-to-member function limitation

933 Views Asked by At

C++ has limited ability to use pointer-to-member functions. I need something that will allow me to dynamically choose a callback member function, in order to use the Visitor pattern of the XMLNode::Accept(XMLVisitor *visitor) method from the TinyXML2 library.

To use XMLNode::Accept(), I must call it with a class which implements the XMLVisitor interface. Hence:

typedef bool (*Callback)(string, string);

class MyVisitor : public tinyxml2::XMLVisitor {
public:
    bool VisitExit(const tinyxml2::XMLElement &e) {
        callback(e.Name(), e.GetText());
    }
    Callback callback;
}

This works fine if my caller is NOT an object which wants to use one of its own methods as a callback function (so that it can access class variables). For example, this works:

bool myCallBackFunc(string e, string v) {
    cout << "Element " << e << " has value " << v << endl;
    return true;
}

int main(...) {
    tinyxml2::XMLDocument doc;
    doc.LoadFile("somefile.xml");
    MyVisitor visit;
    visit.callback = myCallBackFunc;
    doc.Accept(&visit);
}

However, in my use case, the parsing is done inside a method in a class. I have multiple applications which have similar but unique such classes. I'd like to use only one generic MyVisitor class, rather than have the visitor class have unique knowledge of the internals of each class which will call it.

Thus, it would be convenient if the callback function were a method in each calling class so that I can affect the internal state of the object instantiated from that calling class.

Top level: I have 5 server applications which talk to 5 different trading partners, who all send XML responses, but each is enough different that each server app has a class which is unique to that trading partner. I'm trying to follow good OO and DRY design, and avoid extra classes having unique knowledge while still doing basically the same work.

Here's the class method I want Accept() to call back.

ServiceClass::changeState(string elem, string value) {
   // Logic which sets member vars based on element found and its value.
}

Here's the class method which will call Accept() to walk the XML:

ServiceClass::processResponse(string xml) {
    // Parse XML and do something only if certain elements present.

    tinyxml2::XMLDocument doc;
    doc.Parse(xml.c_str(), xml.length());

    MyVisitor visit;
    visit.callback = &changeState; // ERROR.  Does not work.
    visit.callback = &ServiceClass::changeState; // ERROR.  Does not work.
    doc.Accept(&visit);
}

What's a simple way to get what I want? I can imagine more classes with derived classes unique to each situation, but that seems extremely verbose and clumsy.

Note: In the interest of brevity, my sample code above has no error checking, no null checking and may even have minor errors (e.g. treating const char * as a string ;-).

4

There are 4 best solutions below

3
On BEST ANSWER

Below is the std::bind(..) example for what you're trying to do in C++11. For earlier C++ versions you could use the boost::bind utilities.

Fix your MyVisitor::VisitExit(...) method to return a boolean, by the way.

The code is converting const char * to std::string. tinyxml2 does not guarantee that the char * arguments from Name() or GetText() are not null. In fact in my experience they will be null at some point. You should guard against this. For the sake of not modifying your example too much I've not protected against this possibility everywhere in the example.

typedef bool(*Callback)(string, string);
using namespace std;
class MyVisitor : public tinyxml2::XMLVisitor {
public:
    bool VisitExit(const tinyxml2::XMLElement &e) {
    //  return callback(e.Name(), e.GetText());
        return true;
    }
    Callback callback;
};


/** Typedef to hopefully save on confusing syntax later */
typedef std::function< bool(const char * element_name, const char * element_text) > visitor_fn;

class MyBoundVisitor : public tinyxml2::XMLVisitor {
public:
    MyBoundVisitor(visitor_fn fn) : callback(fn) {}

    bool VisitExit(const tinyxml2::XMLElement &e) {
        return callback(e.Name() == nullptr ? "\0" : e.Name(), e.GetText() == nullptr ? "\0": e.GetText());
    }
    visitor_fn callback;
};

bool 
myCallBackFunc(string e, string v) {
    cout << "Element " << e << " has value " << v << endl;
    return true;
} 

int 
main()
{
        tinyxml2::XMLDocument doc;
        doc.LoadFile("somefile.xml");
        MyVisitor visit;
        visit.callback = myCallBackFunc;
        doc.Accept(&visit);

        visitor_fn fn = myCallBackFunc; // copy your function pointer into the std::function<> type
        MyBoundVisitor visit2(fn); // note: declare this outside the Accept(..) , do not use a temporary
        doc.Accept(&visit2);
}

So from within the ServiceClass method you'd do:

ServiceClass::processResponse(string xml) {
    // Parse XML and do something only if certain elements present.

    tinyxml2::XMLDocument doc;
    doc.Parse(xml.c_str(), xml.length());
// presuming changeState(const char *, const char *) here
    visitor_fn fn = std::bind(&ServiceClass::changeState,this,std::placeholders::_1,std::placeholders::_2);
    MyBoundVisitor visit2(fn); // the method pointer is in the fn argument, together with the instance (*this) it is a method for.
    doc.Accept(&visit);
}
0
On

First define a template and a helper function:

namespace detail {
    template<typename F>
    struct xml_visitor : tinyxml2::XMLVisitor {

        xml_visitor(F&& f) : f_(std::move(f)) {}

        virtual void VisitExit(const tinyxml2::XMLElement &e) {
            f_(e);
        }
    private:
        F f_;
    };
}

template<class F>
auto make_xml_visitor(F&& f)
{
    return detail::xml_visitor<std::decay_t<F>>(std::forward<F>(f));
}

Then use the helper function to construct a custom visitor from a lambda which captures this:

void ServiceClass::processResponse(std::string xml) {
    // Parse XML and do something only if certain elements present.

    tinyxml2::XMLDocument doc;
    doc.Parse(xml.c_str(), xml.length());

    auto visit = make_xml_visitor([this](const auto& elem) 
    { 
        this->changeState(elem.Name(), elem.GetText); 
    });
    doc.Accept(std::addressof(visit));
}
5
On

The rule is that a function pointer must always accept a void * which is passed in to the module which calls it, and passed back. Or use a lambda which is the same thing with some of the machinery automated for you. (The void * is the "closure").

So

 typedef bool (*Callback)(string, string, void *context);


  class MyVisitor : public tinyxml2::XMLVisitor {
  public:

      bool VisitExit(const tinyxml2::XMLElement &e) {
          callback(e.Name(), e.GetText(), contextptr);
     }
     Callback callback;
     void *contextptr;
  }

  bool myCallBackFunc(string e, string v, void *context) {
     ServiceClass *service = (ServiceClass *) context; 
     cout << "Element " << e << " has value " << v << endl;
     service->ChangeState(e, v);
     return true;
  }
0
On

You can use generics in order to support whichever callback you'd like.

I've tried to mock the classes of the library in order to give you a fully runnable example:

#include <string>
#include <iostream>
#include <functional>

class XmlNode {
public:
    XmlNode(const std::string& n, const std::string t) : name(n), txt(t) {}

    const std::string& Name() const { return name; }
    const std::string& GetText() const { return txt; }

private:
    std::string name;
    std::string txt;
};

class XMLVisitor {
public:
    virtual void VisitExit(const XmlNode& node) = 0;
    virtual ~XMLVisitor() {}
};

template<typename T>
class MyVisitor : XMLVisitor {
public:
    MyVisitor() {}

    void myInnerPrint(const XmlNode& node) {
        std::cout << "MyVisitor::myInnerPrint" << std::endl;
        std::cout << "node.Name(): " << node.Name() << std::endl;
        std::cout << "node.GetText(): " << node.GetText() << std::endl;
    }

    void SetCallback(T newCallback) {
        callback = newCallback;
    }

    virtual void VisitExit(const XmlNode& node) {
        callback(node);
    }

    T callback;
};

int main() {
    XmlNode node("In", "Member");
    MyVisitor<std::function<void(const XmlNode&)>> myVisitor;
    auto boundCall =
        [&myVisitor](const XmlNode& node) -> void {
        myVisitor.myInnerPrint(node);
    };

    myVisitor.SetCallback(boundCall);
    myVisitor.VisitExit(node);
    return 0;
}