Search code examples
c++tinyxml2

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


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 ;-).


Solution

  • 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);
    }