我很樂意解釋我是如何寫就 DumpEnum 的,甚至給出代碼。此間我還將回答第二個問題。不過首先讓我為其他讀者解釋一下 DumpEnum 是做什么的。在四月發(fā)表的文章中,我要做的一件事情是寫一個 C++ 枚舉來完全充當(dāng) .NET 框架類型 RegexOptions。RegexOptions 是一個枚舉類型,你可以用 Regex::Match 和 Replace 之類的方法來控制匹配。例如,你可以用 RegexOptions::IgnoreCase 來調(diào)用 Regex::Match 以忽略大小寫,或者
RegexOptions::Singleline 來將輸入串作為一行。RegexOptions 的值如 Figure 1 所示。
為了從本機(jī)C++代碼傳遞 RegexOptions,你需要一個具有相同值的 C-式樣枚舉。如果你僅僅是一時之需,那么編寫這樣的代碼最快的方法是從文檔中將之拷貝到你的 C++ 文件,然后按照 C 的語法編輯它。但如果你要進(jìn)行印刷,或修改選項(xiàng),或者想要包裝若干枚舉類型的話,那么最好最可靠的方法是寫一個工具,它能自動生成 C++ 代碼——尤其反射使之更容易;.NET 框架提供了全部描述自身所需的信息。所以我寫了一個小程序 DumpEnum,你可以從命令行運(yùn)行它,像這樣:
DumpEnum RegexOptions
DumpEnum 將名字/值對寫成 C/C++ 代碼送到標(biāo)準(zhǔn)輸出。你可以像下面這樣將輸出重定向到一個文件:
DumpEnum RegexOptions > regopt.h
然后將 regopt.h 插入到你的頭文件。在我的文章中,RegexWrap.h 就是這么做的。DumpEnum 為 RegexOptions 實(shí)際生成的文件如 Figure 2 所示。
現(xiàn)在你知道 DumpEnum 是做什么的了,下面你會明白我是如何實(shí)現(xiàn)它的。
每一個框架類都是由 System.Type 類描述。它具備 Name 屬性以獲取類型名,IsEnum 告之該類型是否為枚舉。DumpEnum 要做的第一件事情是不論什么類型名,都要獲得 Type,把它們傳遞到命令行——例如,RegexOptions,實(shí)現(xiàn)它并不是想像的那么容易。獲得 Type 最普通的兩個方法是:如果已知對象實(shí)例,則調(diào)用 obj->GetType;否則用 __typeof。例如:
#using <System.dll>
using namespace System::Text::RegularExpressions;
...
Type *t = __typeof(RegexOptions);
在 C++ 中使用托管類型必須用 __typeof,因?yàn)?/span> typeof 已經(jīng)有定義(在新的 C++\CLI 中要用 typeid<>)。但 __typeof 只能用于符號名,而不是字符串,這意味著編譯時你得知道名字,以及哪個程序集和其所屬的名字空間。DumpEnum 在編譯時不具備這些信息,所以要從命令行參數(shù)中獲得類型名。DumpEnum 也不具備對象實(shí)例,所以它無法調(diào)用 Object::GetType,怎么辦呢?
還有另一個得到 Type 的方法。如果已知該類型屬于哪個程序集,那么可以加載它并調(diào)用 Assembly::GetType,它有一個串參數(shù)。但 Assembly::GetType 需要完整的類型名,并且你得知道要加載哪個程序集。DumpEnum 給我的第一個痛是要我在命令行敲入下面這些信息:
DumpEnum System.dll System.Text.RegularExpressions.RegexOptions
你知道 RegexOptions 在 System.dll 中,而且你也知道名字空間是 System.Text.RegularExpressions,因?yàn)槲臋n就是這么說的。看文檔是一件很惱人的事,而且敲入這個完整的名字空間足以讓你得上腕關(guān)節(jié)綜合癥。我寧愿花三小時寫一個只需敲12個字符而不是54個字符的程序,而不原意花30秒鐘來查找DLL/namespace和敲入完整類型名。這似乎與懶惰者有些自相矛盾,但這是好程序員的精神特質(zhì)所需要的,因?yàn)槟阕罱K是要做出一個很棒的工具永久使用——并且能夠與朋友們分享!
在按照我的想法寫 DumpEnum 之前,必須解決幾個基本問題,其中包括本文的第二個提問:已知類型名(可能是非限定名),如:RegexOptions,如何知道哪個框架 DLL 包含該類型?沒有集中的數(shù)據(jù)庫或保存此信息的注冊表項(xiàng)(這也是一件好事情),或方法調(diào)用,看來是沒有希望了。但是等一等——再想想!所有的框架 DLLs 都在一個文件夾中,我最近一次數(shù)過也就七十來個。為什么強(qiáng)力使用之呢?只要將每個程序集加載到框架環(huán)境,查找到那個名字與期望的類型名匹配者。這樣很簡單直白,并且在我低檔的 1 GHz P3 上只需幾秒鐘。我寫了一個程序叫 FindType 就是做此事的:
FindType MenuItem
如圖 Figure 3 所示。FindType 列出所有名字中帶 MenuItem 類型輸出的框架 DLLs。FindType 查找包含該單詞文本的類型。換句話說,如果你運(yùn)行“FindType Control”,那么 FindType 將輸出 System.Windows.Forms.Control 和 System.Web.UI.Control,但不包括 System.Web.UI.WebControls.WebControl。這個思路假設(shè)你輸入的是簡稱名字,也就是你在 #using 指令中使用的名字空間。FindType 通過建立“\bControl\b”這樣的正則表達(dá)式來實(shí)現(xiàn)。特殊的錨點(diǎn)字符(原子零寬度斷言)''\b''用于單詞分隔,而不用再匹配中包含分隔符。

Figure 3 FindType 的運(yùn)行結(jié)果
FindType 是如何知道要搜索哪個 DLLs 呢?所有框架 DLLs 都在一個名叫“C:\WINDOWS\Microsoft.NET\Framework\v1.1.4322”的文件夾中。你可以通過環(huán)境變量 %FrameworkDir% (實(shí)際解析出來是“C:\WINDOWS\Microsoft.NET\Framework”)和 %FrameworkVersion%(“v1.1.4322”)來獲取自己機(jī)器上實(shí)際路徑。為了簡單起見,我在 CFindType 類中將此過程進(jìn)行了封裝。為了使用這個類,你得派生自己專用的特例并實(shí)現(xiàn)虛擬函數(shù) OnMatch:
class CMyFindType : public CFindType {
protected:
virtual BOOL OnMatch(LPCTSTR typName, LPCTSTR asmPath)
{
// print it
}
};
然后實(shí)例化并調(diào)用 FindExact 方法:
CMyFindType ft;
int nFound = ft.FindExact("MenuItem");
CFindType 遍歷程序集,查找與傳入的名字相匹配的類型。只要找到一個匹配類型,便用該類型和程序集名調(diào)用你的虛擬 OnMatch 處理例程。如果你想用不同的正則表達(dá)式,你可以調(diào)用 CFindType::Find 代替 FindExact。CFindType 類使 FindType 程序的實(shí)現(xiàn)變得容易:只要寫一個 OnMatch,將信息輸出到標(biāo)準(zhǔn)輸出 stdout 即可。具體細(xì)節(jié)請參考本文附帶的源代碼。
CFindType 本身派生于一個更為一般的類,CEunmTypes,它枚舉框架中所有的類型,針對每個類型調(diào)用虛擬 OnType 函數(shù)。CFindType::OnType 用 Regex 比較類型名和請求的名字,并調(diào)用 OnMarch 檢查其是否匹配。CEnumTypes 用 _tgetenv 來獲得環(huán)境變量以建立型如“FrameworkDir\FrameworkVersion\*.dll”的文件規(guī)范,然后用 MFC 的 CFileFind 類枚舉 DLLs。CEunmTypes 試圖將每個 DLL 作為程序集加載。如果加載失敗(也許該 DLL 不是一個托管程序集),CEnumTypes 則忽略它并繼續(xù)搜索。如果加載成功,便調(diào)用 Assembly::GetExportedTypes 來獲取程序集輸出的 Type 數(shù)組,然后針對每個 Type 調(diào)用 OnType。代碼如 Figure 4 所示。
現(xiàn)在有了 CFindType,我終于可以用它來解決最初的問題:修改 DumpEnum,以便我不用非得告訴它程序集和完整的類型名。DumpEnum 以不同的 OnMarch 處理例程使用 CFindType。DumpEnum 中的處理例程檢查該類型確實(shí)在枚舉的類型中(Type::IsEnum 返回 True),如果真是如此,則按照 C++ 代碼方式取出枚舉名/值對,如果 Figure 2 所示。DumpIt 函數(shù)實(shí)際的工作內(nèi)容如 Figure 5 所示。DumpIt 用 Enum::GetUnderlyingType 獲取枚舉的底層類型(一般是 System.Int32),用 Enum::GetValues 獲取枚舉值,用 Convert::ChangeType 將枚舉值轉(zhuǎn)換為其對應(yīng)的底層類型。以下是輸出顯示名字/值對的代碼:
Type* entype = // 托管枚舉類型
Array* values = Enum::GetValues(entype);
for (int i=0; iLength; i++) {
Object* enval = values->GetValue(i);
Object* unval = Convert::ChangeType(enval, untype);
_tprintf(_T("\t%s = %s,\n"), enval, unval);
}
現(xiàn)在你一舉兩得:FindType 使你能找到某個特定類型包含在哪個 DLL/程序集;DumpEnum 生成 C/C++ 代碼來包裝框架枚舉類型。
繼續(xù)討論之前,本文有一個小插曲,我寫完 FindType 之后,發(fā)現(xiàn)一個令人沮喪的事情,在 Visual Studio .NET 中已經(jīng)附帶有一個 FindType 程序,甚至名字都一樣!微軟版本的 FindType 所做的事情和我的相同,并且做得更好。(老天啊,微軟的老大們捷足先登;我暈倒!)
微軟的 FindType 具備選項(xiàng)來指定完全或部分匹配要搜索的目錄和名字空間,是否顯示方法,屬性,事件等等。Figure 6 是它列出所有選項(xiàng)的幫助屏幕:

Figure 6 Visual Studio 附帶的 FindType
如果你想了解更多關(guān)于反射,程序集和類型的內(nèi)容,或者自在周五晚上無所事事,FindType 是個很好的學(xué)習(xí)研究例子。你可以在 VS.NET\SDK\v1.1\Samples\Applications\TypeFinder 目錄找到它的源碼,它是用 C# 和 Visual Basic 寫的。我生成了 C# 版本,并將其 EXE 文件拷貝到我的 bin 目錄;現(xiàn)在我都是用 FindType 來查找類型的。
是不是有了微軟的 FindType,我自己的版本就毫無用處了呢?當(dāng)然不是。例如,CFindType 和 CEnumTypes
是類,不是程序,也就是說你可以用它們來寫自己的查找類型信息的工具以及像 DumpEnum 這樣的程序。況且我的類是用中最好的編程語言 C++ 寫的。而微軟的版本用的則是 Visual Basic 和 C#。如果你決定在自己打大應(yīng)用程序中使用 CFindType,那么應(yīng)該注意到 CFindType 不用在枚舉時考慮卸載程序集。因?yàn)槊總€程序集都會消耗相當(dāng)多的內(nèi)存,你可能會考慮修改這個實(shí)現(xiàn),將每個程序集加載到其自己的應(yīng)用程序域中,然后在完成程序集的搜索之后卸載應(yīng)用程序域。
我正在用一個基于模板的庫源代碼,該庫包含一些針對特定類型的模板函數(shù)特化。類模板,函數(shù)模板和模板函數(shù)特化都在頭文件中。我在我的.cpp文件中 #include 頭文件并編譯鏈接工程。但是為了在整個工程中使用該庫,我將頭文件包含在 stdafx.h 中,結(jié)果出現(xiàn)特化模板函數(shù)的符號多重定義錯誤。我要如何組織頭文件才能避免多重符號定義錯誤?我用 /FORCE:MULTIPLE,但我想用一個更好的解決方法。
Lee Kyung Jun
實(shí)際上,確實(shí)用更好的解決方法。稍后我會解釋,但首先讓我重溫一下模板函數(shù)特化是如何工作的。假設(shè)你有一個比較兩個基于 operator> 和 operator== 對象的模板函數(shù):
template <typename T>
int compare(T t1, T t2)
{
return t1==t2 ? 0 : t1 > t2 ? 1 : -1;
}
該模板根據(jù)地一個參數(shù)是否等于、大于、或小于第二個參數(shù)而分別返回零或+/-1。它是典型的用于集合排序時的排序函數(shù)。它假設(shè)類型 T 具備 operator== 和 operator> 操作,并支持 int,float,double 或 DWORD 類型。但它不能應(yīng)用于比較自負(fù)串(char* 指針),因?yàn)檫@個函數(shù)比較的是串指針,而不是字符串本身:
LPCTSTR s1,s2;
...
int cmp = compare(s1,s2); // s1<s2? Oops!
為了能進(jìn)行字符串比較,你需要一個使用 strcmp 或其 TCHAR 版本 _tcscmp 的模板特化:
// specialization for strings
template<>
int compare<LPCTSTR>(LPCTSTR s1, LPCTSTR s2)
{
return _tcscmp(s1, s2);
}
沒錯,這樣做完全正確,現(xiàn)在的問題是:將這個特化放在何處?顯然是要放在模板的頭文件中。但這樣會導(dǎo)致符號多重定義的錯誤,就像 Lee 遇到的那樣。原因很明顯,模板特化是一個函數(shù),而非模板。它與下面的寫法是一樣的:
int compare(LPCTSTR s1, LPCTSTR s2)
{
return _tcscmp(s1, s2);
}
沒有理由不在頭文件中定義函數(shù)——但是一旦這樣做了,那么你便無法在多個文件中 #include 該頭文件。至少,肯定會有鏈接錯誤。怎么辦呢?
如果你掌握了模板函數(shù)特化即函數(shù),而非模板的概念,你就會認(rèn)識到有三個選項(xiàng),完全與普通函數(shù)一樣;特化為 inline,extern 或者 static。例如,像下面這樣:
template<>
inline int compare<LPCTSTR>(LPCTSTR s1, LPCTSTR s2)
{
return _tcscmp(s1, s2);
}
對于大多數(shù)模板庫而言,這是最容易和最常見的解決方案。因?yàn)榫幾g器直接擴(kuò)展內(nèi)聯(lián)函數(shù),不產(chǎn)生外部符號,在多個模塊中 #include 它們沒有什么問題。鏈接器不會出錯,因?yàn)椴淮嬖诙嘀囟x的符號。對于像 compare 這樣的小函數(shù)來說,inline 怎么說都是你想要的(它更快)。
但是,如果你的特化很長,或出于某種原因,你不想讓它成為 inline,那要如何做呢?此時可以做成 extern。語法與常規(guī)函數(shù)一樣:
// in .h header file
template<>
extern int compare<LPCTSTR>(LPCTSTR s1, LPCTSTR s2);
當(dāng)然,你得在某個地方實(shí)現(xiàn) compare。部分細(xì)節(jié)如 Figure 7 所示。我在單獨(dú)的模塊 Templ.cpp 中實(shí)現(xiàn)了特化,它與主工程鏈接。Templ.h 被 #include 在 stdafx.h 中,而 stdafx.h 又被 #include 在 Templ.cpp 和主模塊兩個文件中——生成工程沒有鏈接錯誤。去下載源代碼自己嘗試一下吧。
如果你正在為其他開發(fā)人員寫模板庫,extern 方式會很不爽,因?yàn)槟惚仨殑?chuàng)建一個帶目標(biāo)模塊的鏈接庫(lib),它包含有特化。如果你已經(jīng)有了一個這樣的 .lib,也沒什么;如果沒有,你可能會想方設(shè)法避免引入這樣的庫。僅用頭文件實(shí)現(xiàn)模板是更好的方法(麻煩少)。最容易的方式是用 inline,此外,你還能將你的特化放在單獨(dú)的頭文件中,使之與其聲明分開并要其他開發(fā)人員只在一個模塊中 #include 特化。還有一個可選的方法是將所有東西放在一個文件中,并用預(yù)處理符號控制實(shí)例化:
#ifdef MYLIB_IMPLEMENT_FUNCS
template<>
int compare<LPCTSTR>(LPCTSTR s1, LPCTSTR s2)
{
return _tcscmp(s1, s2);
}
#endif
使用該方法,所有模塊都包含此頭文件,但在包含它之前,只有一個 #define MYLIB_IMPLEMENT_FUNCS。這個方法不支持預(yù)編譯頭,因?yàn)榫幾g器用 stdafx.h 中的任何 MYLIB_IMPLEMENT_FUNCS 值加載預(yù)編譯版本。
避免符號多重定義錯誤的最后同時也是用得最少的一個方法是將特化做成 static:
template<>
static int compare<LPCTSTR>(LPCTSTR s1, LPCTSTR s2)
{
return _tcscmp(s1, s2);
}
這樣鏈接器也不會出錯,因?yàn)殪o態(tài)函數(shù)不向外界輸出其函數(shù),并且它讓你將所有東西都保持在一個頭文件中,不用引入預(yù)處理符號。但它缺乏效率,因?yàn)槊總€模塊都有一個函數(shù)拷貝。如果函數(shù)小到?jīng)]什么——那為何不用內(nèi)聯(lián)呢?
所以簡言之:將特化做成 inline 或 extern。通常都是用 inline。兩種方法都得編輯頭文件。如果使用的是第三方的庫沒有頭文件,那么你除了用鏈接選項(xiàng) /FORCE:MULTIPLE 之外別無選擇。在你等著生成你的工程時,你可以告訴編寫庫文件的那個家伙——為什么要將函數(shù)模板特化定義成 inline 或者 extern。就說是我說的。
您的提問和評論可發(fā)送到 Paul 的信箱:cppqa@microsoft.com.