1

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

4 回答 4

4

下面是您在 C++11 中尝试执行的操作的 std::bind(..) 示例。对于早期的 C++ 版本,您可以使用 boost::bind 实用程序。

顺便说一句,修复您的MyVisitor::VisitExit(...)方法以返回布尔值。

代码正在转换const char *std::string. tinyxml2 不保证char *参数 fromName()GetText()不为空。事实上,根据我的经验,它们在某些时候会为空。你应该提防这种情况。为了不过多地修改您的示例,我没有在示例中的任何地方防止这种可能性。

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

因此,您可以在 ServiceClass 方法中执行以下操作:

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);
}
于 2016-12-16T19:04:15.533 回答
0

您可以使用泛型来支持您想要的任何回调。

我试图模拟库的类,以便为您提供一个完全可运行的示例:

#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;
}
于 2016-12-16T19:46:31.393 回答
0

首先定义一个模板和一个辅助函数:

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

然后使用辅助函数从捕获的 lambda 构造自定义访问者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));
}
于 2016-12-16T22:30:15.480 回答
-2

规则是函数指针必须始终接受一个 void *,它被传递给调用它的模块,然后传回。或者使用 lambda,这与一些为您自动化的机器是一样的。(void * 是“闭包”)。

所以

 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;
  }
于 2016-12-16T19:16:52.633 回答