不要被這個標題唬住了,實際上我是非常認可Qt的。在C++實現的開源產品中沒有哪一個的API風格比得上Qt,擁有高度一致性,符合常識,符合直覺,幾乎不用學就可以直接上手。或許是由於我們擺脫不了馬太效應的控制,贊譽已經給到了Qt的缺陷。Qt的最大問題就是提供了uic和moc。界面設計器生成xml文件,需要uic編譯它生成C++代碼;組織界面的C++代碼其實一點都不復雜,完全可以由界面設計器直接生成。可以給Qt找到需要uic的理由——實現了分工,可以並行,為設計器開發團隊屏蔽了C++語法的復雜性。然而,uic相對於界面設計器來說,工作量幾乎可以忽略不記,在管理實踐上如此不平衡的分工沒有任何意義,並行也就說不過去了。組織界面的C++代碼完全掌控在Qt團隊手里,完全可以用最簡單的方式實現(uic生成的c++代碼也確實非常簡單),這樣一來也沒有什么“C++語法的復雜性”需要屏蔽了。如果uic對用戶來說沒有壞處,僅僅給Qt團隊增加了工作量,也就無可非議了。但是,uic集成到第三方開發工具中時,導致設計器創建的界面,不能及時生成為C++代碼,必須手動執行一下uic。
uic跟moc比起來,就是小巫見大巫了。提供moc的原因,很大一部分是因為信號和槽機制。每每聽到有人帶着無比崇敬的態度布道Qt的信號和槽機制,真希望他們能知道信號和槽到底為了什么而存在。還是先來看一段Qt的代碼吧。
class QDataSourceWidget : public QTreeView { Q_OBJECT public: explicit QDataSourceWidget(QWidget *parent = 0); ~QDataSourceWidget(); signals: void LayerAdded(IMapPtr, ILayerPtr, ILayerProviderPtr); protected: virtual void LayerAddEvent(IMapPtr map, ILayerPtr layer, ILayerProviderPtr provider) { emit LayerAdded(map,layer,layerProvider); } private slots: void NodeDoubleClicked(const QModelIndex &index) { ... LayerAddEvent(map,layer,provider); } };
這段代碼要完成這樣一個功能:當表示數據源的QTreeView的節點雙擊時,打開數據,為數據創建一個可視化圖層添加到map中,然后對外發布一個已經添加新圖層的消息。很簡單的一個功能,看看為了實現它Qt提供了些什么?4個擴展關鍵字——Q_OBJECT、signals、slots、emit;3個需要注冊到QMetaTypes的自定義類型IMapPtr、ILayerPtr、ILayerProviderPtr(這3種類型實際上是另外3種類型的指針,但是必須得typedef才能注冊到QMetaTypes中);1個元編譯器moc。
非常代價高昂的解決方案,連編譯器這種重型武器都上場了。為什么需要編譯器?這段代碼已經不可以再被稱作C++了,就像.NET平台下的c++ cli一樣,已經基於C++擴展出了一門新語言。眾所周知C++的編譯器非常難寫,通常新標准發布10年之后都不被完全支持,這跟C++語義重載過多、語法自相矛盾、機制過於復雜不無關系。元編譯器沒有直接生成機器碼,而是將“Qt c++”編譯成了能夠實現信號和槽機制的標准C++,再由C++編譯器編譯成機器碼。這種方案確實避免了面對不同架構不同版本CPU的麻煩,但是仍然需要面對“Qt c++”中的C++成分。這就是偶爾會遇到元編譯器執行失敗、元編譯器生成的C++代碼編譯不過的原因。這些現象在新版本Qt中確有很大改觀,但是C++標准委員會並沒有浪子回頭的意思,元編譯器即將面對的是更多語義重載、更多語法矛盾、更復雜的機制。
其實“Qt c++”也沒怎么擴展標准C++,就多了4個關鍵字而已,而這4個關鍵字就是要派重武器——編譯器——上場的罪魁禍首之一。這4個關鍵字還起了另外一個壞作用,讓針對標准C++的代碼自動格式化、代碼自動完成失效了。對於需要注冊QMetaTypes倒沒有什么好抱怨的,畢竟帶來了其它好處。
那么Qt以如此之高代價實現的信號和槽機制到底是什么高檔玩意呢?說白了就是一種發布/訂閱機制而已,對於沒有從語言層面上支持調用棧上的發布/訂閱機制的編程語言來說,一般通過兩種方式來實現——Listener模式和回調函數。java swing就是典型的Listener模式,這個很顯然;如果說MFC的Message Map是回調函數,可能會遇到爭議。Message Map提供了消息碼到消息處理函數指針的映射,消息循環從Message Map中查找到處理某個消息的所有函數指針,然后依次調用。消息循環是框架提供的,只是通過Message Map的形式傳進去一個函數指針而已,雖然沒有直接調用SetXXXCallBack,不影響它仍然是回調函數。
Qt為何棄這兩種方式不用呢?確實有說得過去的理由。首先,C++沒有匿名類,沒有垃圾回收機制。如果采用Listener模式,必須得為每個不同簽名的消息至少定義一個類,必須得合理地管理這些Listener的生命周期。MFC的Message Map方式,需要在代碼中寫很多宏,在不考慮代碼自動完成時,顯然只寫下signals和slots兩個關鍵字更為方便。從而,Qt便倉促地選擇了由GTK發明的信號和槽的概念。(注:這是筆者幫Qt想的理由,是否還有其它理由筆者沒有深入了解。)MFC的Message Map還是有他的擁躉的,wxWidgets便是其中較為知名的一員。
其實完全可以通過C++實現比Message Map更好的回調機制。在提出實現方式之前先明確一下需求和約束。
首先是需求,第一,訂閱方可以是成員函數、靜態函數、C函數、仿函數;第二,發布方可以支持多個訂閱者同時訂閱。如果滿足這兩條需求就已經比Qt的信號和槽機制要強大了。
當然也有一些約束,第一,既然發布方可以支持多個訂閱者同時訂閱,那么發布方若要采納訂閱者的返回值的話到底應該采納哪一個的,這是個問題,所以干脆讓訂閱者全都返回void(Qt目前支持返回非void類型,但是有什么卵用他們內部仍然有爭議);第二,輕量級,不用stl,不用boost(不用boost還說得過去,stl畢竟是c++的標准庫。我有我的理由,C++的缺陷導致編譯器特別難寫,可以說在模板方面找不到實現地完全正確的編譯器。一些編譯器不能正確的為靜態的或者全局的模板類變量生成構造代碼,這應該是Google代碼規范禁止這么做的原因,全局的和靜態的類只能聲明為指針,由程序員確保其被正確地構造出來。)。加上這兩條約束,仍然不影響滿足前兩條需求的發布/訂閱機制比Qt的信號和槽機制強大。
接下來給出完全通過C++實現的比Message Map和信號/槽機制更強大更輕量級的回調實現機制。
首先給出返回值是void類型可以代表成員函數、靜態函數、C函數、仿函數的訂閱者接口定義。這里用到了C++11的可變模板參數機制,只是為了方便而已。要支持C++98,可以用typelist機制或者直接多定義幾個不同參數數量的模板。推薦用后者,typelist可能有些編譯器支持不了。
template<typename ...Args> struct IEventHandler { virtual void operator()(Args&... args) = 0; IEventHandler() {}; virtual ~IEventHandler() {}; private: IEventHandler(const IEventHandler &) = delete; IEventHandler &operator=(const IEventHandler &) = delete; };
接下來支持靜態函數、C函數、仿函數的訂閱者實現。
template<typename Callable, typename ...Args> class CallableEventHandler : public IEventHandler<Args...> { public: CallableEventHandler(Callable handler) { _handler = handler; } virtual ~CallableEventHandler() {}; public: void operator()(Args&... args) { _handler(args...); } private: Callable _handler; };
然后,支持成員函數的訂閱者實現。
template<typename T, typename ...Args> class EventHandler : public IEventHandler<Args...> { public: typedef void(T::*Handler)(Args...); EventHandler(T* receiver, Handler handler) { _receiver = receiver; _handler = handler; } virtual ~EventHandler() {}; public: void operator()(Args&... args) { (_receiver->*_handler)(args...); } private: Handler _handler; T* _receiver; };
最后,發布方實現。
template<typename ...Args> class Event { public: typedef IEventHandler<Args...>* Callable; public: Event() { _valid = false; _event = nullptr; }; Event(const Callable& h) { _handler = h; _valid = true; _event = nullptr; } ~Event() { if (_event != nullptr) { delete _event; } }; const Event<Args...>& operator = (const Callable& h) { _handler = h; _valid = true; if (_event != nullptr) { delete _event; _event = nullptr; } return *this; } Event(const Event<Args...> & e) { this->Add(e); } Event<Args...> &operator=(const Event<Args...> & e) { _valid = false; if (_event != nullptr) { delete _event; _event = nullptr; } this->Add(e); return *this; } public: void Raise(Args&... args) { if (_valid) { (*_handler)(args...); } if (_event != nullptr) { _event->Raise(args...); } } void operator()(Args&... args) { this->Raise(args...); } public: void Add(const Callable& h) { if (_valid) { if (_event != nullptr) { _event->Add(h); } else { _event = new Event<Args...>(h); } } else { _handler = h; _valid = true; } } void Remove(const Callable& h) { if (_valid && _handler == h) { if (_event == nullptr) { _valid = false; } else { Event<Args...>* event_ = _event; _valid = _event->_valid; _handler = _event->_handler; _event = _event->_event; event_->_event = nullptr; delete event_; } } else if (_event != nullptr) { _event->Remove(h); } } void Add(const Event<Args...>& e) { Event<Args...>* event_ = const_cast<Event<Args...>*>(&e); while (event_ != nullptr) { if (event_->_valid) { this->Add(event_->_handler); } event_ = event_->_event; } } void Remove(const Event<Args...>& e) { Event<Args...>* event_ = const_cast<Event<Args...>*>(&e); while (event_ != nullptr) { if (event_->_valid) { this->Remove(event_->_handler); } event_ = event_->_event; } } public: const Event<Args...>& operator += (const Callable& h) { this->Add(h); return *this; } const Event<Args...>& operator -= (const Callable& h) { this->Remove(h); return *this; } const Event<Args...>& operator += (const Event<Args...>& e) { this->Add(e); return *this; } const Event<Args...>& operator -= (const Event<Args...>& e) { this->Remove(e); return *this; } private: bool _valid; Callable _handler; Event<Args...>* _event; };
僅此而已,加上很多空白行才有不到300行代碼。這是我在開源項目tGis實現的發布/訂閱機制,可以采用如下方式使用。
Event SomeEvent; EventHandler handler; SomeEvent += &handler; // 綁定訂閱者到發布者方式一 SomeEvent.Add(&handler); // 綁定訂閱者到發布者方式二 SomeEvent += new EventHandler; // 要綁定訂閱者到發布者方式三,暫不支持,會導致內存泄漏 SomeEvent(); // 觸發事件方式一 SomeEvent.Raise(); //觸發事件方式二
當然,這個實現方式仍然有些不足。第一,參數不能是右值;觸發事件的函數參數不能直接作為事件的參數,不能在事件參數上直接構造對象,而需要先聲明個變量接收一下。這個不足解決起來也很簡單,加個接收右值參數的重載就行了。(這個不足只是對C++11而言的)
Event SomeEvent; SomeEvent(SomeClass()); // C++11中,這樣觸發事件是錯誤的,編譯不過 SomeClass some; SomeEvent(some); // 這樣一定是可以的
第二,對訂閱者進行了包裝,但是沒有提供生命周期管理機制。這只是需求和代碼量的權衡,我不想就為支持“+= new”式的語法搞出一套生命周期管理機制來。簡單的實現方式是auto_ptr,如果願意,讀者可以自己實現。在這里,我要善意的提醒一句,c++里的堆是開發庫提供的,很可能不同的鏈接庫以及執行文件中的堆不是一個堆,用一個堆的delete去刪除另一個堆里的對象將會收獲一個程序崩潰。
希望這里提到的缺陷不要影響到讀者的技術選型。優勢就是優勢,缺陷就是缺陷;不能優勢大於缺陷之后缺陷也成為了優勢,技術人應該有客觀的技術態度。要知道我是在用Qt的,參考開源項目tGis。其實信號和槽機制沒有給用戶帶來什么麻煩,僅僅是給Qt團隊帶來了巨大的麻煩而已。