2016-12-16 5 views
1

C++ имеет ограниченную возможность использовать функции указателя на элемент. Мне нужно что-то, что позволит мне динамически выбирать функцию-функцию обратного вызова, чтобы использовать шаблон Visitor метода XMLNode::Accept(XMLVisitor *visitor) из библиотеки TinyXML2.Как работать с ограничением функции указателя на объект C++

Чтобы использовать XMLNode::Accept(), я должен назвать его классом, который реализует интерфейс XMLVisitor. Следовательно:

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

class MyVisitor : public tinyxml2::XMLVisitor { 
public: 
    bool VisitExit(const tinyxml2::XMLElement &e) { 
     callback(e.Name(), e.GetText()); 
    } 
    Callback 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); 
} 

Однако в моем случае использования, синтаксический анализ делается внутри метода в классе. У меня есть несколько приложений, которые имеют похожие, но уникальные классы. Я бы хотел использовать только один общий класс MyVisitor, вместо того, чтобы класс посетителя имел уникальное знание внутренних компонентов каждого класса, который будет его называть.

Таким образом, было бы удобно, если бы функция обратного вызова была методом в каждом вызывающем классе, чтобы я мог влиять на внутреннее состояние объекта, созданного из этого вызывающего класса.

Верхний уровень: У меня есть 5 серверных приложений, которые говорят с 5 различными торговыми партнерами, которые все отправляют ответы XML, но каждый из них достаточно различен, что каждое приложение сервера имеет класс, уникальный для этого торгового партнера. Я стараюсь следить за хорошим дизайном OO и DRY и избегать дополнительных классов, обладающих уникальными знаниями, в то же время выполняющих в основном ту же работу.

Вот метод класса, я хочу, чтобы Accept() перезвонить.

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

Вот метод класса, который будет вызывать Accept() ходить 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); 
} 

Что простой способ получить то, что я хочу? Я могу представить себе больше классов с производными классами, уникальными для каждой ситуации, но это кажется чрезвычайно многословным и неуклюжим.

Примечание. В интересах краткости мой примерный код выше не имеет проверки ошибок, не имеет нулевой проверки и может даже иметь незначительные ошибки (например, обработка const char * в виде строки ;-).

+1

Вы пробовали std :: bind? –

ответ

4

Ниже приведен пример std :: bind (..) для того, что вы пытаетесь сделать в C++ 11. Для более ранних версий C++ вы можете использовать утилиты boost :: bind.

Устраните ваш метод MyVisitor::VisitExit(...), чтобы вернуть логическое значение, между прочим.

Код преобразует const char * в std::string. tinyxml2 не гарантирует, что аргументы char * от Name() или 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); 
} 
+0

Нет, я на самом деле не слепо преобразовываю const char * в std :: string. Аналогично, я удалил все ошибки и нулевые проверки. Я пытался сделать мои примеры кода настолько маленькими, насколько это возможно, но все еще проиллюстрировать проблему. Я раньше не использовал std :: bind и посмотрю. – CXJ

+0

Является ли это опечаткой? 'visitor_fn fn = std :: string (& ServiceClass:'? Не должно быть 'visitor_fn fn = std :: bind (& ServiceClass:' вместо? – CXJ

+0

Да, это правильно. Я исправлю позже. – JimmyNJ

-2

Правило состоит в том, что указатель на функцию всегда должна принять пустоту *, которая передается в к модулю, который вызывает его, и вернулся. Или используйте лямбда, что является одним и тем же с некоторыми автоматизированными машинами для вас. (Пустота * является «закрытием»).

Так

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

Может ли кто-то проголосовать за этот ответ, пожалуйста, объясните, почему? Если есть технический недостаток, я бы хотел его понять. Функции указателя на член являются сложными и сложными для многих людей. – CXJ

+0

Я не проголосовал за это, но использование void * для переноса указателя функции обычно считается кодом низкого качества, и поскольку в нем есть бросок (синтаксис C), внутри функция myCallBackFunc с тем типом, который ваш вопрос хотел отвлечь, он ничего не делает для вас. – JimmyNJ

+0

Это недостаток с C++. Современный способ - использовать лямбда. Пустота * - это способ настройкилямбда-закрытие вручную. void * не является указателем на функцию, а является контекстом для указателя функции. Однако вызываемый агностик в отношении void * (на самом деле контекст часто равен нулю), абстракция не является вашей проблемой. Он уверен, что пройденные и полученные типы действительно совпадают. –

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

Затем использовать вспомогательная функция для создания пользовательского посетителя из лямбда, которая фиксирует 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)); 
}