1. 程式人生 > >作為一個c++而非c程式設計師,該棄函式指標投函式物件了

作為一個c++而非c程式設計師,該棄函式指標投函式物件了

我想把mqtt的c庫封裝為符合面向物件的形式,其中有個函式是

DLLExport int MQTTClient_setCallbacks(MQTTClient handle, void* context, MQTTClient_connectionLost* cl,
        MQTTClient_messageArrived* ma, MQTTClient_deliveryComplete* dc);

是用來設定訂閱後情況發生時回撥函式的。主要關心argv[2]和argv[3]即連線斷開或訊息到達時的處理回撥。
typedef void MQTTClient_connectionLost(void* context, char* cause);
typedef int MQTTClient_messageArrived(void* context, char* topicName, int topicLen, MQTTClient_message* message);

連線斷開時應該呼叫connect、subscribe重新訂閱;訊息到達時應該將topic、payload封裝為string型別,然後釋放底層庫的記憶體,然後呼叫使用者之前設定的回撥函式,並將topic、payload作為引數以string型別傳入。所以設想中封裝類應該長這樣:
typedef void (*MsgArrvdHandler)(const std::string, const std::string);
class GMqttClient {
public:

    void connect(const bool& ssl, const size_t& timeout) {
        ...
    }

    void disconnect() {
        ...
    }

    void setMsgarrvdCallback(const MsgArrvdHandler& msgarrvd) {
        m_msgarrvd = msgarrvd;
        MQTTClient_setCallbacks(m_client, NULL, connlost, _msgarrvd, NULL);
    }

    void subscribe(const std::string& topicFilter) {
        ...
    }

private:

    MsgArrvdHandler m_msgarrvd; // 用來存放使用者的回撥函式

    static void Free(void *topicName, MQTTClient_message* message) {
        ...
    }

    void connlost(void *context, char *cause) {
        connect(this->m_ssl, this->m_conn_opts.connectTimeout);
        subscribe(this->m_topicFilter);
    }
    // 當MQ到達時被mqtt庫呼叫的回撥函式
    int _msgarrvd(void *context, char *topicName, int topicLen, MQTTClient_message *message) {
        string topic(topicName);
        string msg((char*) message->payload, message->payloadlen);
        Free(topicName, message);
        // 呼叫使用者的回撥函式,並將topic和payload傳入
        (this->m_msgarrvd)(topic, msg);
        return 1;
    }

    std::string m_topicFilter;
    bool m_ssl;
    MQTTClient m_client;
    MQTTClient_connectOptions m_conn_opts;
};

使用者這樣使用:
void MsgArrvd(const std::string& topic, const std::string& payload) {
    cout << "message arrived: " << payload << endl;
}

int main() {
    GMqttClient sub;
    sub.setMsgarrvdCallback(&MsgArrvd);
    sub.connect();
    sub.subscribe();
    ...

    return 0;
}

想是這樣想的,但是實現起來不簡單。類的非靜態成員函式是不能作為函式指標傳入MQTTClient_setCallbacks中的,所以我的_msgarrvd和connlost不能直接傳入。這時我想到了boost::bind和boost::function,bind可以把類的非靜態成員函式繫結到function型別的函式物件上。如下:
class A {
public:

    void func(const string& arg) {
        cout << arg << endl;
    }
};

int main(int argc, char* argv[]) {
    try {
        A a;
        boost::function<void(const string&) > func2 = boost::bind(&A::func, a, _1);
        func2("bind");

    } catch (const std::exception& e) {
        std::cout << e.what() << std::endl;
    }

    cout << "done" << endl;
    return 0;
}

輸出:
bind
done

於是我想這麼改造setMsgarrvdCallback:

void setMsgarrvdCallback(const MsgArrvdHandler& msgarrvd) {
    m_msgarrvd = msgarrvd;

    // MQTTClient_setCallbacks(m_client, NULL, connlost, _msgarrvd, NULL);		
    boost::function<int(void*, char*, int, MQTTClient_message*) > functmp = boost::bind(&GMqttClient::_msgarrvd, this, _1, _2, _3, _4);
    MQTTClient_setCallbacks(m_client, NULL, NULL, functmp, NULL);
}

但是編譯報錯:
error: cannot convert ‘boost::function<int(void*, char*, int, MQTTClient_message*)>’ to ‘int (*)(void*, char*, int, MQTTClient_message*)’
想想也對,宣告很明顯不一樣,編譯當然過不了。看來只能把GMqttClient::_msgarrvd改造為靜態方法,然後把this指標傳入,再呼叫使用者的回撥函數了:
class GMqttClient {

    void setMsgarrvdCallback(const MsgArrvdHandler& msgarrvd) {
        m_msgarrvd = msgarrvd;
        MQTTClient_setCallbacks(m_client, this, connlost, _msgarrvd, NULL);
    }

    static int _msgarrvd(void *context, char *topicName, int topicLen, MQTTClient_message *message) {
        GMqttClient* asyncSubscriber = (GMqttClient*) context;
        string topic(topicName);
        string msg((char*) message->payload, message->payloadlen);
        Free(topicName, message);
        (asyncSubscriber->m_msgarrvd)(topic, msg);
        return 1;
    }
    ...
};

但是問題還沒解決,我這樣封裝,使用者只能傳入一個函式指標,要是他也想像我之前那樣傳入一個非靜態成員函式呢?之前那個問題遞迴了。。。這時候bind / function終於派上用場了。把 MsgArrvdHandler m_msgarrvd 改為function函式物件:

typedef boost::function<void(const std::string&, const std::string&)> MsgArrvdHandlerF;
MsgArrvdHandlerF m_msgarrvd;

setMsgarrvdCallback引數從函式指標也改為function函式物件,這樣使用者可以通過bind傳入非靜態成員函式。然後在 static GMqttClient::int _msgarrvd(...) 中呼叫m_msgarrvd:
typedef boost::function<void(const std::string&, const std::string&) > MsgArrvdHandlerF;

class GMqttClient {

    void setMsgarrvdCallback(const MsgArrvdHandler& msgarrvd) {
        m_msgarrvd = msgarrvd;
        MQTTClient_setCallbacks(m_client, this, connlost, _msgarrvd, NULL);
    }

    static int _msgarrvd(void *context, char *topicName, int topicLen, MQTTClient_message *message) {
        GMqttClient* asyncSubscriber = (GMqttClient*) context;
        string topic(topicName);
        string msg((char*) message->payload, message->payloadlen);
        Free(topicName, message);
        (asyncSubscriber->m_msgarrvd)(topic, msg);
        return 1;
    }
    MsgArrvdHandlerF m_msgarrvd;
    ...
};

class UserClass {
    void run() {
        try {
            m_MQClient.setMsgarrvdCallback(boost::bind(&UserClass::msgArrvdHandler, this, _1, _2));
            m_MQClient.connect();
            m_MQClient.subscribe();
            ...
        } catch (exception &e) {
            cout << "ERROR: " << e.what() << endl;
        }
    }
    void msgArrvdHandler(const string& topic, const string& payload) {
        cout << "message arrived: " << payload << endl;
        ...
    }
    GMqttClient m_MQClient;
};
總結:

外經常這樣宣傳bind:bind介面可以輕鬆實現類成員函式作為回撥函式。確實,bind後的函式物件可以直接呼叫,但是卻不能賦值給普通函式指標,所以,任何一個具有非同步呼叫功能的函式,當把回撥函式型別宣告為普通函式指標,那麼你永遠不能把類的非靜態成員函式直接作為引數傳入。要想實現類非靜態成員函式作為回撥函式被直接非同步呼叫,回撥函式引數型別必須為function函式物件,而非傳統的函式指標。也可以像我上面的例子繞過這個問題:定義一個靜態成員函式,把物件指標和靜態成員函式作為函式指標傳入具有非同步呼叫功能的函式,在靜態成員函式中通過物件指標呼叫非靜態成員函式。