C++編譯期函式/變數檢測技術,模擬VC關鍵字__if_exists
轉帖請註明出處 http://www.cppblog.com/cexer/archive/2008/07/06/55484.html
VC當中有一個鮮為人知的關鍵字,除了微軟自己的程式碼,我從未在任何地方看到有人用過它。雖然它的功能很強大,不過除非設計上的問題或是一些無法排除的困難,否則幾乎從不會需要用到它的功能。但是有時候,它確實能作為一個最簡單的解決方案而讓某些設計過程事半功倍。
借用 CCTV10《走近科學》的語氣:那麼這個神祕的關鍵關鍵字到底是什麼呢?它又實現了什麼神奇的功能呢?帶著這一連串的疑問,讓我們先來看一個具體的例子。
我在自己曾經寫的一個GUI框架當中,為了實現訊息與處理函式自動對映的,就需要求助於這種功能。比如說有一個視窗類,它包含若干訊息處理函式和一個訊息與處理函式的對映 map:(請無視當中的 show() 和 create() 函式,與主題無關)
class Window
{
typedef UINT _Message;
typedef LRESULT (Window::*_Handler)(_Message);
map<_Message,_Handler> m_handlerMap;
public:
bool show();
bool create();
public:
LRESULT onEvent( WindowEvent<WM_CREATE> );
LRESULT onEvent( WindowEvent<WM_DESTROY> );
};
我需要利用模板超程式設計 從 0 到 WM_USER 進行迴圈檢測,檢測 Window 類是否存在該訊息對應的處理函式。如果訊息對應的處理函式存在,那麼就將訊息與函式的對映放進 m_handlerMap 當中。比如說訊息 WM_CREATE,我檢測類 Window是否存在 LRESULT onEvent( WindowEvent<WM_CREATE> ) 成員函式,在上例程式碼中是存在的,於是我將這樣一個對映放進m_handlerMap:(真正實現的時候,還要考慮函式的型別。不同型別的函式,是不能直接裝進 map 當中的。不過在這裡請無視例子當中涉及的所有型別轉換,與主題無關)
pair<WM_CREATE,&Window::onEvent>
這樣就達到了訊息自動對映的目的。而不用像MFC一樣手寫巨集去對映。(最後通過努力的確達到了我的目的,我的GUI框架能夠進行自動訊息映射了,然而可以預見,由於幾千個(0-WM_USER)迴圈,編譯期的速度受到極大影響。所以最終我還是拋棄了這種自動對映實現,而採用了更高效神奇的方法,這是後話也與本主題無關就先不提)。
要實現以上的自動對映功能就引出了這樣一個難題:如何編譯期檢測類的某特定名字的成員是否存在。
功能不負有心人,經過爬山涉水翻山越嶺,我終於在 MSDN 一個偏遠角落裡找著了傳說當中那個神祕的關鍵字:__if_exists(其實還有一個 __if_not_exists)。MSDN 當中這樣說明:__if_exists (__if_not_exists)允許你針對某符號的存在與否條件性地執行語句。使用語法:(注意檢測的是“存在性”,而不是值)
__if_exists ( /*你要檢測存在性的函式或變數的名字*/ ) {
//做些有用的事
}
MSDN當中的示例程式碼如下:
// the__if_exists_statement.cpp
// compile with: /EHsc
#include <iostream>
template<typename T>
class X : public T {
public:
void Dump() {
std::cout << "In X<T>::Dump()" << std::endl;
__if_exists(T::Dump) {
T::Dump();
}
__if_not_exists(T::Dump) {
std::cout << "T::Dump does not exist" << std::endl;
}
}
};
class A {
public:
void Dump() {
std::cout << "In A::Dump()" << std::endl;
}
};
class B {};
bool g_bFlag = true;
class C {
public:
void f(int);
void f(double);
};
int main() {
X<A> x1;
X<B> x2;
x1.Dump();
x2.Dump();
__if_exists(::g_bFlag) {
std::cout << "g_bFlag = " << g_bFlag << std::endl;
}
__if_exists(C::f) {
std::cout << "C::f exists" << std::endl;
}
return 0;
}
以上程式碼的輸出如下:(未測試,此輸出為MSDN的說明文件當中的)
In X<T>::Dump()
In A::Dump()
In X<T>::Dump()
T::Dump does not exist
g_bFlag = 1
C::f exists
大概很少人見過這個關鍵字吧。雖然它們的功能與我的需求是如此的接近,但是面對如此強憾的關鍵字,我還是隻能搖頭嘆息。我傷心地在文件裡看到說明,__if_exists(__if_not_exists)關鍵字用於函式的時候,只能根據函式名字進行檢測,而會忽略對引數列表的檢測,因此沒有對過載函式的分辨能力,而正是我需要的。比如類 Window 有一個函式:
LRESULT Window::onEvent( WindowEvent<WM_DESTROY> )
{
//做些有用的事
}
我用以下程式碼來檢測 WM_CREATE 訊息是否存在處理函式:
__if_exists(Window::onEvent)
{
//新增訊息對映
}
即使 Window 類當中不存在 LRESULT onEvent ( WindowEvent<WM_CREATE> ),以上測試也能通過。這是因為 __if_exists 關鍵字是不管函式過載的,如果存在一個 onEvent ,那麼所有的檢測都能通過。這不是我想要的。我需要比 __if_exists 更強憾的檢測功能,強憾到能夠針對不同引數列表的同名函式(過載函式)做出正確的存在性測試。
於是我繼續翻山越嶺地尋找,從 CSDN 到 MSDN,從 SourceForge 到 CodeProject。要相信那句老話:“有心人天不負”。最後我在 CodeProject 上面看到一篇讓我醍醐灌頂的文章:
這篇文章從原理到實現,很詳細地說明地一種編譯期檢測技術,先說明一下,由於VC7.1數千個bug當中的一個,以下技術不能在VC++7.1或更低版本上使用。具體的實現在那篇文章當中說得很詳盡了,還是在這兒贅述一下。
Alexandre Courpron的實現方式基於C++的這樣一個規則:Substitution Failure Is Not An Error(簡稱SFINAE)。它的含義我也理解得比較含糊,不過它作用於過載函式的時候,可以這樣理解:對於一個函式呼叫,在匹配函式的過程當中,如果最終能夠有一個函式匹配成功,那麼對其餘函式的匹配如果失敗,編譯器也不會視為錯誤。聽起來有些麻煩,看Alexandre Courpron給出的例子:
struct Test
{
typedef int Type;
};
template < typename T >
void f(typename T::Type) {} // definition #1
template<typename T>
void f(T){} // definition #2
f<Test>(10); //call #1
f<int>(10); //call #2
對於 call#1 編譯器直接匹配 definition#1 成功。對於 call#2,編譯器先用 definition#1 匹配 如下:
void f( typename int::Type ) {}
這顯然是不正確的。不過編譯器並沒有編譯失敗報告錯誤,因為下面的 definition#2 匹配成功,根據 SFINAE的 規則,編譯器有權保持沉默 。
雖然是個小小的規則,在平時幾乎不會注意它。然而在這兒,我們卻可以利用它實現編譯期檢測的強大功能了,一個最簡單的示例:
#include <iostream>
using namespace std;
//
struct TestClass
{
void testFun();
};
struct Exists { char x;};
struct NotExists { char x[2]; };
template <void (TestClass::*)()>
struct Param ;
template <class T>
Exists isExists( Param<&T::testFun>* );
template <class T>
NotExists isExists( ... );
//
int main()
{
cout<<sizeof(isExists<TestClass>(0))<<endl;
}
上面的程式碼會輸出 1。說明一下檢測的過程:
- 編譯器遇到 isExists<TestClass>(0) 這一句,會去匹配 isExists 的兩個過載函式。不定長的引數優先順序更低,因此先匹配第一個函式。
- 第一個函式引數型別為 Param<&T::testFun>*,在這裡是 Param<&TestClass::testFun>,編譯器在匹配這個引數型別的時候會嘗試例項化模板類 Param。
- 編譯器嘗試用 &TestClass::testFun 去例項化 Param,因為 TestClass 確實存在一個 void (TestClass::*)() 型別,且名為 testFun 的成員函式。所以 Param 的例項化成功,因此引數匹配成功。
- 匹配第一個函式成功。編譯器決定 isExists<TestClass>(0) 這一句呼叫就是呼叫的第一個函式。
- 因為第一個函式返回的型別為 Exists,用 sizeof 取大小就是 1。
如果是我們把 TestClass 的定義修改為:(僅把函式的引數型別改為 int )
struct TestClass
{
void testFun(int);
};
這一次程式碼會輸出 2。因為在第3步的時候,由於 TestClass 沒有型別為 void (TestClass::*)(),且名為 testFun 的函式,所以例項化 Param 會失敗,因此匹配第一個函式失敗。然後編譯器去匹配第二個函式。因為其引數型別是任意的,自然會匹配成功。結果會輸出 2。
當然這只是個最簡單的示例,通過模板包裝類。可以實現更靈活更強大的功能。比如回到那個自動訊息對映的例子,用以下程式碼就能夠實現了:
//c++std
#include <iostream>
using namespace std;
//windows
#include <windows.h>
//detector
template<typename TWindow,UINT t_msg>
struct MessageHandlerDetector
{
typedef WindowEvent<t_msg> _Event;
struct Exists {char x;};
struct NotExists {char x[2];};
template<LRESULT (TWindow::*)(_Event)>
struct Param;
template<typename T>
static Exists detect( Param<&T::onEvent>* );
template<typename T>
static NotExists detect( ... );
public:
enum{isExists=sizeof(detect<TWindow>(0))==sizeof(Exists)};
};
//test classes
struct Window
{
LRESULT onEvent( WindowEvent<WM_CREATE> );
};
struct Button
{
LRESULT onEvent( WindowEvent<WM_DESTROY> );
};
//main
int main()
{
cout<<MessageHandlerDetector<Window,WM_CREATE>::isExists<<endl;
cout<<MessageHandlerDetector<Window,WM_DESTROY>::isExists<<endl;
cout<<MessageHandlerDetector<Button,WM_CREATE>::isExists<<endl;
cout<<MessageHandlerDetector<Button,WM_DESTROY>::isExists<<endl;
return 0;
}
以上程式碼會輸出:
1
0
0
1
以上的示例程式碼再加上模板超程式設計,可以很輕易地實現訊息的自動對映,具體實現這個已不在本貼的討論範圍並且這種自動對映的實現,太過複雜,在編譯期沒有效率,且不夠靈活。不過在訊息對映機制上來說,已稱得上是一種革命性的嘗試。
在說完了這所有一切之後,再告訴你一個我最近才知道的祕密(不準笑我孤陋寡聞):其實 boost 庫當中已有相關功能的 MPL 工具存在,叫做 has_xxx。
原始檔:<boost\mpl\has_xxx.hpp>