C++/CLI:第一流的CLI語言
2005-08-25 11:25 作者: 朱先忠編譯 出處: 天極網(wǎng) 責(zé)任編輯:方舟
1. 簡介
本文并不是為了奉承C++/CLI的輝煌,也不是為了貶低其它如C#或者VB.NET等語言,相反,這只是一個(gè)非官方的、以一個(gè)喜歡這種語言的非微軟雇員身份來論證C++/CLI有它的自己的唯一的角色,可作為第一流的.NET編程語言。
一個(gè)不斷在新聞組和技術(shù)論壇上出現(xiàn)的問題是,當(dāng)象C#和VB.NET這樣的語言更適合于這種用途時(shí),為什么要使用C++來開發(fā).NET應(yīng)用軟件。通常這樣一些問題后面的評論說是,C++語法是怎樣的復(fù)雜和令人費(fèi)解,C++現(xiàn)在是怎樣一種過時(shí)的語言,還有什么VS.NET設(shè)計(jì)者已不再像支持C#和VB.NET一樣繼續(xù)支持C++。其中一些猜疑是完全荒謬的,但有些說法部分正確。希望本文有助于澄清所有這些圍繞C++/CLI語言及其在VS.NET語言層次中的地位的疑惑,神秘和不信任。請記住,本作者既不為微軟工作也沒有從微軟那里取得報(bào)酬,只是想從技術(shù)上對C++/CLI作一評判。
2. 快速簡潔的本機(jī)interop
除了P/Invoke機(jī)制可用在另外的象C#或VB.NET這樣的語言外,C++提供了一種獨(dú)有的interop機(jī)制,稱作C++ interop。C++ interop比P/Invoke直觀得多,因?yàn)槟阒皇呛唵蔚?include需要的頭文件,并與需要的庫進(jìn)行鏈接就能象在本機(jī)C++中一樣調(diào)用任何函數(shù)。另外,它比P/Invoke速度快--這是很容易能證明的。現(xiàn)在,可爭辯的是在實(shí)際應(yīng)用軟件的開發(fā)中,經(jīng)由C++ interop獲得的性能好處與花在用戶接口交互、數(shù)據(jù)庫存取、網(wǎng)絡(luò)數(shù)據(jù)轉(zhuǎn)儲(chǔ)、復(fù)雜數(shù)學(xué)算法等方面的時(shí)間相比可以被忽略,但是事實(shí)是在有些情況下,甚至通過每次interop調(diào)用節(jié)省的幾個(gè)納秒也能給全局應(yīng)用程序性能/響應(yīng)造成巨大影響,這是絕對不能被忽視的。下面有兩部分代碼片斷(一個(gè)是使用P/Invoke機(jī)制的C#程序,一個(gè)是使用C++ Interop機(jī)制的C++程序),我分別記錄了其各自代碼重復(fù)執(zhí)行消耗的時(shí)間(毫秒)。不管你如何解釋這些數(shù)據(jù),不管這會(huì)對你的應(yīng)用程序產(chǎn)生什么影響,全是你的事。我僅打算事實(shí)性地指出,C++代碼的執(zhí)行速度要比C#(其中使用了較多的本機(jī)interop調(diào)用)快。
1) C#程序(使用P/Invoke)
[SuppressUnmanagedCodeSecurity] [DllImport("kernel32.dll")] static extern uint GetTickCount(); [SuppressUnmanagedCodeSecurity] [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)] static extern uint GetWindowsDirectory( [Out] StringBuilder lpBuffer, uint uSize); static void Test(int x) { StringBuilder sb = new StringBuilder(512); for (int i = 0; i < x; i++) GetWindowsDirectory(sb, 511); } static void DoTest(int x) { uint init = GetTickCount(); Test(x); uint tot = GetTickCount() - init; Console.WriteLine("Took {0} milli-seconds for {1} iterations",tot, x); } static void Main(string[] args) { DoTest(50000);DoTest(500000);DoTest(1000000);DoTest(5000000); Console.ReadKey(true); } |
2) C++程序(使用C++ Interop)
void Test(int x) { TCHAR buff[512]; for(int i=0; i<x; i++) GetWindowsDirectory(buff, 511); } void DoTest(int x) { DWORD init = GetTickCount(); Test(x); DWORD tot = GetTickCount() - init; Console::WriteLine("Took {0} milli-seconds for {1} iterations",tot, x); } int main(array<System::String ^> ^args) { DoTest(50000);DoTest(500000);DoTest(1000000);DoTest(5000000); Console::ReadKey(true); return 0; } |
3) 速度比較
重復(fù)次數(shù) |
C# 程序 |
C++程序 |
50,000 |
61 |
10 |
500,000 |
600 |
70 |
1,000,000 |
1162 |
140 |
5,000,000 |
6369 |
721 |
其性能差別真是令人驚愕!這的確是說明為什么要使用C++/CLI的一個(gè)好理由,如果你在使用本機(jī)interop進(jìn)行開發(fā),那么性能!完全由于性能,我就將被迫借助本機(jī)interop來實(shí)現(xiàn)并非基于web的.NET應(yīng)用程序。當(dāng)然,為什么我想要使用.NET來開發(fā)需要大量本機(jī)interop技術(shù)的應(yīng)用程序完全是另外一個(gè)問題。
如果你仍懷疑這種性能優(yōu)勢,有另外的理由來說明你為什么不得不使用C++/CLI而不是C#或VB.NET——源碼膨脹!下面是一個(gè)C++函數(shù)的例子,它使用了IP幫助者API來枚舉一臺(tái)機(jī)器上的網(wǎng)絡(luò)適配器并且列出與每個(gè)適配器相聯(lián)系的所有IP地址。
4) 枚舉n/w適配器的C++代碼
void ShowAdapInfo() { PIP_ADAPTER_INFO pAdapterInfo = NULL; ULONG OutBufLen = 0; //得到需要的緩沖區(qū)大小 if(GetAdaptersInfo(NULL,&OutBufLen)==ERROR_BUFFER_OVERFLOW) { int divisor = sizeof IP_ADAPTER_INFO; #if _MSC_VER >= 1400 if( sizeof time_t == 8 ) divisor -= 8; #endif pAdapterInfo = new IP_ADAPTER_INFO[OutBufLen/divisor]; //取得適配器信息 if( GetAdaptersInfo(pAdapterInfo, &OutBufLen) != ERROR_SUCCESS ) {//調(diào)用失敗 } else { int index = 0; while(pAdapterInfo) { Console::WriteLine(gcnew String(pAdapterInfo->Description)); Console::WriteLine("IP Address list : "); PIP_ADDR_STRING pIpStr = &pAdapterInfo->IpAddressList; while(pIpStr) { Console::WriteLine(gcnew tring(pIpStr->IpAddress.String)); pIpStr = pIpStr->Next; } pAdapterInfo = pAdapterInfo->Next; Console::WriteLine(); } } delete[] pAdapterInfo; } } |
現(xiàn)在讓我們看一個(gè)使用P/Invoke的C#版本。
5) 使用P/Invoke技術(shù)的C#版本
const int MAX_ADAPTER_NAME_LENGTH = 256; const int MAX_ADAPTER_DESCRIPTION_LENGTH = 128; const int MAX_ADAPTER_ADDRESS_LENGTH = 8; const int ERROR_BUFFER_OVERFLOW = 111; const int ERROR_SUCCESS = 0; [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)] public struct IP_ADDRESS_STRING { [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 16)] public string Address; } [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)] public struct IP_ADDR_STRING { public IntPtr Next; public IP_ADDRESS_STRING IpAddress; public IP_ADDRESS_STRING Mask; public Int32 Context; } [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)] public struct IP_ADAPTER_INFO { public IntPtr Next; public Int32 ComboIndex; [MarshalAs(UnmanagedType.ByValTStr, SizeConst = MAX_ADAPTER_NAME_LENGTH + 4)] public string AdapterName; [MarshalAs(UnmanagedType.ByValTStr, SizeConst = MAX_ADAPTER_DESCRIPTION_LENGTH + 4)] public string AdapterDescription; public UInt32 AddressLength; [MarshalAs(UnmanagedType.ByValArray, SizeConst = MAX_ADAPTER_ADDRESS_LENGTH)] public byte[] Address; public Int32 Index; public UInt32 Type; public UInt32 DhcpEnabled; public IntPtr CurrentIpAddress; public IP_ADDR_STRING IpAddressList; public IP_ADDR_STRING GatewayList; public IP_ADDR_STRING DhcpServer; public bool HaveWins; public IP_ADDR_STRING PrimaryWinsServer; public IP_ADDR_STRING SecondaryWinsServer; public Int32 LeaseObtained; public Int32 LeaseExpires; } [DllImport("iphlpapi.dll", CharSet = CharSet.Ansi)] public static extern int GetAdaptersInfo(IntPtr pAdapterInfo, ref int pBufOutLen); static void ShowAdapInfo() { int OutBufLen = 0; //得到需要的緩沖區(qū)大小 if( GetAdaptersInfo(IntPtr.Zero, ref OutBufLen) == ERROR_BUFFER_OVERFLOW ) { IntPtr pAdapterInfo = Marshal.AllocHGlobal(OutBufLen); //取得適配器信息 if( GetAdaptersInfo(pAdapterInfo, ref OutBufLen) != ERROR_SUCCESS ) { //調(diào)用失敗了 } else{ while(pAdapterInfo != IntPtr.Zero) { IP_ADAPTER_INFO adapinfo = (IP_ADAPTER_INFO)Marshal.PtrToStructure( pAdapterInfo, typeof(IP_ADAPTER_INFO)); Console.WriteLine(adapinfo.AdapterDescription); Console.WriteLine("IP Address list : "); IP_ADDR_STRING pIpStr = adapinfo.IpAddressList; while (true){ Console.WriteLine(pIpStr.IpAddress.Address); IntPtr pNext = pIpStr.Next; if (pNext == IntPtr.Zero) break; pIpStr = (IP_ADDR_STRING)Marshal.PtrToStructure( pNext, typeof(IP_ADDR_STRING)); } pAdapterInfo = adapinfo.Next; Console.WriteLine(); } } Marshal.FreeHGlobal(pAdapterInfo); } } |
3. 棧語義和確定性的析構(gòu)
C++經(jīng)由棧語義模仿給了我們確定性的析構(gòu)。簡言之,棧語義是Dispose模式的良好的語法替代品。但是它在語義上比C# using塊語法更直觀些。請看下列的C#和C++代碼段(都做一樣的事情-連接兩個(gè)文件的內(nèi)容并把它寫到第三個(gè)文件中)。
1) C#代碼--使用塊語義
public static void ConcatFilestoFile(String file1, String file2, String outfile) { String str; try{ using (StreamReader tr1 = new StreamReader(file1)) { using (StreamReader tr2 = new StreamReader(file2)) { using (StreamWriter sw = new StreamWriter(outfile)) { while ((str = tr1.ReadLine()) != null) sw.WriteLine(str); while ((str = tr2.ReadLine()) != null) sw.WriteLine(str); } } } } catch (Exception e) { Console.WriteLine(e.Message); } } |
2) C++代碼--棧語義
static void ConcatFilestoFile(String^ file1, String^ file2, String^ outfile) { String^ str; try{ StreamReader tr1(file1); StreamReader tr2(file2); StreamWriter sw(outfile); while(str = tr1.ReadLine()) sw.WriteLine(str); while(str = tr2.ReadLine()) sw.WriteLine(str); } catch(Exception^ e) { Console::WriteLine(e->Message); } } |
C#代碼與相等的C++ 代碼相比不僅免不了冗長,而且using塊語法讓程序員自己明確地指定他想在哪兒調(diào)用Dispose(using塊的結(jié)束處),而使用C++/CLI的棧語義,只需讓編譯器使用常規(guī)的范圍規(guī)則來處理它即可。事實(shí)上,這使得在C#中修改代碼比在C++中更乏味-作為一實(shí)例,讓我們修改這些代碼以便即使僅存在一個(gè)輸入文件也能創(chuàng)建輸出文件。請看下面修改后的C#和C++代碼。
3) 修改后的C#代碼
public static void ConcatFilestoFile(String file1, String file2, String outfile) { String str; try{ using (StreamWriter sw = new StreamWriter(outfile)) { try{ using (StreamReader tr1 = new StreamReader(file1)) { while ((str = tr1.ReadLine()) != null) sw.WriteLine(str); } } catch (Exception) { } using (StreamReader tr2 = new StreamReader(file2)) { while ((str = tr2.ReadLine()) != null) sw.WriteLine(str); } } } catch (Exception e){ } } |
把針對StreamWriter的using塊放到頂層需要重新調(diào)整using塊結(jié)構(gòu)--這在上面情況下顯然不是個(gè)大問題,但是對于實(shí)際開發(fā)中的修改,這可能是相當(dāng)模糊的且易導(dǎo)致邏輯錯(cuò)誤的。
4) 修改后的C++代碼
static void ConcatFilestoFile(String^ file1, String^ file2, String^ outfile) { String^ str; try{ StreamWriter sw(outfile); try{ StreamReader tr1(file1); while(str = tr1.ReadLine()) sw.WriteLine(str); } catch(Exception^){} StreamReader tr2(file2); while(str = tr2.ReadLine()) sw.WriteLine(str); } catch(Exception^){} } |
這樣不是比在C#中的做更容易些嗎?我恰好把StreamWriter聲明移到了頂部并增加了一個(gè)額外的try塊,就這些。甚至對于象在我的示例代碼片斷中的瑣碎事情,如果所涉及的復(fù)雜性在C++中大大減少,那么,當(dāng)你工作于更大的工程時(shí)你能想象使用棧語義對你的編碼效率千萬的影響。
還不確信?好,讓我們看一下成員對象和它們的析構(gòu)吧。Imagine CLI GC類R1和R2,二者都實(shí)現(xiàn)了Idisposable接口且都有函數(shù)F(),還有一個(gè)CLI GC類R,它有R1和R2成員和一個(gè)函數(shù)F()-它內(nèi)部地調(diào)用R1和R2上的F()成員函數(shù)。讓我們先看C#實(shí)現(xiàn)。
5) 一個(gè)disposable類繼承層次的C#實(shí)現(xiàn)
class R1 : IDisposable{ public void Dispose() { } public void F() { } } class R2 : IDisposable{ public void Dispose() { } public void F() { } } class R : IDisposable{ R1 m_r1 = new R1(); R2 m_r2 = new R2(); public void Dispose() { m_r1.Dispose(); m_r2.Dispose(); } public void F() { m_r1.F(); m_r2.F(); } public static void CallR() { using(R r = new R()) {r.F();} } } |
這里有幾件事情要做:必須為每個(gè)disposable 類手工實(shí)現(xiàn)IDisposable接口,對于具有成員R1和R2的類R,Dispose方法也需要調(diào)用成員類上的Dispose。現(xiàn)在讓我們分析上面幾個(gè)類的C++實(shí)現(xiàn)。
6) 等價(jià)的C++實(shí)現(xiàn)
ref class R1 { public: ~R1(){} void F(){} }; ref class R2 { public: ~R2(){} void F(){} }; ref class R { R1 m_r1; R2 m_r2; public: ~R(){} void F() { m_r1.F(); m_r2.F(); } static void CallR() { R r; r.F(); } }; |
注意,這里不再有手工的Idisposable接口實(shí)現(xiàn)(我們的類中僅建立了析構(gòu)器)而且最好的部分--類R的析構(gòu)器(Dispose方法)并沒有在該類可能含有的可釋放的成員上調(diào)用Dispose-它沒有必要這樣做,編譯器自動(dòng)為之生成所有的代碼!
4. 混合類型
我們知道,C++支持本機(jī)類型-總是如此;C++支持CLI類型-本文正是特別強(qiáng)調(diào)這一點(diǎn);它還支持混合類型-具有CLI成員的本機(jī)類型和具有本機(jī)成員的CLI類型!請盡管考慮所有你能的可能需求。
注意,談到Whidbey,混合類型實(shí)現(xiàn)還不完整;就我從Brandon,Herb和Ronald發(fā)表的材料的理解得知,存在這種相當(dāng)酷的類型--統(tǒng)一模型,它將在Orcas中實(shí)現(xiàn)--你能夠在本機(jī)C++堆上new/delete CLI類型,而且也能夠在CLI堆上gcnew/delete本機(jī)類型。但既然這是Whidbey以后的東西,本文不討論統(tǒng)一模型。
在我談?wù)撃愫螘r(shí)使用混合類型以前,我想向你說明什么是混合類型。如果你理解混合類型,請?zhí)^下面幾段。這里引用Brandon Bray的說法:"一種混合類型,或者是本機(jī)類ref類(需要有對象成員),或者是通過聲明或繼承被分配在垃圾回收堆或本機(jī)堆上的。"因此如果你有一個(gè)托管類型或者有一個(gè)有托管成員的本機(jī)類型,你就有了一個(gè)混合類型。VC++ Whidbey不直接支持混合類型(統(tǒng)一類型模型是一種Whidbey之后的概念),但是它給我們劃定了實(shí)現(xiàn)混合類型的條件。讓我們開始討論包含托管成員的本機(jī)類型。
ref class R { public: void F(){} //假定 non-trivial ctor/dtor R(){} ~R(){} }; |
在我的例子中,設(shè)想該托管類型R有一個(gè)non-trivial構(gòu)造器和一個(gè)non-trivial析構(gòu)器。
class Native { private: gcroot<R^> m_ref; public: Native(): m_ref(gcnew R()){} ~Native() { delete m_ref; } void DoF() { m_ref->F(); } }; |
既然,我不能在我的類中擁有一個(gè)R成員,我使用了gcroot模板類(在gcroot.h中聲明,但是你要用"#include vcclr.h"),它包裝了System::Runtime::InteropServices::GCHandle結(jié)構(gòu)。它是個(gè)象類一樣的靈敏指針,它重載了運(yùn)算符->以返回用作模板參數(shù)的托管類型。因此在上面類中,我可以使用m_ref,就好象我已經(jīng)聲明它是R^,而且你能在DoF函數(shù)中看到這正在起作用。實(shí)際上你可以節(jié)省delete,這可以通過使用auto_gcroot(類似于std::auto_ptr,在msclr\auto_gcroot.h文件中聲明)代替gcroot來實(shí)現(xiàn)。下面是一個(gè)更好些的使用auto_gcroot的實(shí)現(xiàn)。
class NativeEx { private: msclr::auto_gcroot<R^> m_ref; public: NativeEx() : m_ref(gcnew R()){} void DoF() { m_ref->F(); } }; |
下面讓我們看相反的情形:一個(gè)CLI類的本機(jī)成員。
ref class Managed { private: Native* m_nat; public: Managed():m_nat(new Native()){ } ~Managed() { delete m_nat; } !Managed() { delete m_nat; #ifdef _DEBUG throw gcnew Exception("Oh, finalizer got called!"); #endif } void DoF() { m_nat->DoF(); } }; |
我不能定義一個(gè)Native對象來作為一個(gè)ref類成員,因此需要使用一個(gè)Native*對象來代替。我在構(gòu)造器中new該Native對象,然后在析構(gòu)器和finalizer中delete它。如果你運(yùn)行該工程的調(diào)試版,在執(zhí)行到finalizer時(shí)將拋出一個(gè)異常- 因此開發(fā)者可以馬上添加一個(gè)對delete的調(diào)用或?yàn)樗腃LI類型使用棧語義技術(shù)。奇怪的是,庫開發(fā)小組沒有建立一個(gè)gcroot的反向?qū)崿F(xiàn)-但這不是個(gè)大問題,我們可以自己寫。
template<typename T> ref class nativeroot { T* m_t; public: nativeroot():m_t(new T){} nativeroot(T* t):m_t(t){} T* operator->() { return m_t; } protected: ~nativeroot() { delete m_t; } !nativeroot() { delete m_t; #ifdef _DEBUG throw gcnew Exception("Uh oh, finalizer got called!"); #endif } }; |
這僅是個(gè)相當(dāng)簡單的靈敏指針實(shí)現(xiàn),就象一個(gè)負(fù)責(zé)本機(jī)對象分配/回收的ref類。不管怎樣,借助nativeroot模板類,我們可以如下修改托管類:
ref class ManagedEx { private: nativeroot<Native> m_nat; public: void DoF() { m_nat->DoF(); } }; |
好,關(guān)于混合類型的最大問題是什么呢?你可能問。最大問題是,現(xiàn)在你能混合使用你的MFC、ATL、WTL、STL代碼倉庫和.NET框架,并用可能的最直接的方式-只需寫你的混合模式代碼并編譯實(shí)現(xiàn)!你可以建立在一個(gè)DLL庫中建立MFC 類,然后建立一個(gè).NET應(yīng)用程序來調(diào)用這個(gè)DLL,還需要把.NET類成員添加到你的MFC類(也實(shí)現(xiàn)可以相反的情況)。
作為一例,設(shè)想你有一MFC對話框--它通過一個(gè)多行的編輯框接受來自用戶的數(shù)據(jù)-現(xiàn)在,你有一新的要求-顯示一個(gè)只讀編輯框,它將顯示當(dāng)前在該多行編輯框中文本的md5哈希結(jié)果。你的隊(duì)友正在悲嘆他們將必須花費(fèi)幾個(gè)小時(shí)鉆研crypto API,而你的上司在擔(dān)憂你們可能必須要買一個(gè)第三方加密庫;那正是你在他們面前樹立形象的時(shí)候,你宣布你將在15分鐘內(nèi)做完這項(xiàng)任務(wù)。下面是解決的辦法:
添加一個(gè)新的編輯框到你的對話框資源中,并且添加相應(yīng)的DDX變量。選擇/clr編譯模式并且添加下列代碼到你的對話框的頭文件中:
#include <msclr\auto_gcroot.h> using namespace System::Security::Cryptography; |
使用auto_gcroot模板來聲明一個(gè)MD5CryptoServiceProvider成員:
protected: msclr::auto_gcroot<MD5CryptoServiceProvider^> md5; |
在OnInitDialog過程中,gcnew MD5CryptoServiceProvider成員。
md5 = gcnew MD5CryptoServiceProvider(); |
并且為多行編輯框添加一個(gè)EN_CHANGE處理器:
void CXxxxxxDlg::OnEnChangeEdit1() { using namespace System; CString str; m_mesgedit.GetWindowText(str); array<Byte>^ data = gcnew array<Byte>(str.GetLength()); for(int i=0; i<str.GetLength(); i++) data[i] = static_cast<Byte>(str[i]); array<Byte>^ hash = md5->ComputeHash(data); CString strhash; for each(Byte b in hash) { str.Format(_T("%2X "),b); strhash += str; } m_md5edit.SetWindowText(strhash); } |
這里使用了混合類型:一個(gè)本機(jī)Cdialog派生類,該類含有一個(gè)MD5CryptoServiceProvider成員(CLI類型)。你可以輕易地試驗(yàn)相反的情況(如早期的代碼片斷已顯示的)——可以建立一個(gè)Windows表單應(yīng)用程序而且可能想利用一個(gè)本機(jī)類庫--這不成問題,使用上面定義的模板nativeroot即可。
5. 托管模板
也許你對泛型的概念已很清楚了,它幫助你避免進(jìn)入C++的模板夢魘,它是實(shí)現(xiàn)模板的最佳方式,等等。好,假設(shè)這些全部正確,C++/CLI支持泛型就象任何其它CLI語言一樣-但是它有而其它一些CLI語言還沒有的是它還支持托管模板-也就是模板化的ref和value類。如果你以前從未使用過模板,你不能一下欣賞這么多優(yōu)點(diǎn),但是如果你有模板使用背景而且你已發(fā)現(xiàn)了泛型中存在的可能限制你編碼的方式,托管模板將會(huì)大大減輕你的負(fù)擔(dān)。你能聯(lián)合使用泛型和模板- 事實(shí)上有可能用一個(gè)托管類型的模板參數(shù)來實(shí)例化一個(gè)泛型類型(盡管相反的情形是不可能的,因?yàn)檫\(yùn)行時(shí)刻實(shí)例化由泛型所用)。STL.NET (或STL/CLR)以后討論,請很好地利用泛型和托管模板的混合編程吧。
泛型使用的子類型約束機(jī)制將防止你寫出下面的代碼:
generic<typename T> T Add(T t1, T t2) { return t1 + t2; } |
編譯錯(cuò)誤:
error C2676: binary ’+’ : ’T’ does not define this operator or a conversion to a type acceptable to the predefined operator |
現(xiàn)在請看相應(yīng)的模板版本:
template<typename T> T Add(T t1, T t2) { return t1 + t2; } |
那么就可以這樣做:
int x1 = 10, x2 = 20; int xsum = Add<int>(x1, x2); |
還可以這樣做:
ref class R { int x; public: R(int n):x(n){} R^ operator+(R^ r) { return gcnew R(x + r->x); } }; //... R^ r1 = gcnew R(10); R^ r2 = gcnew R(20); R^ rsum = Add<R^>(r1, r2); |
這在一個(gè)象int的本機(jī)類型以及一個(gè)ref類型(只要ref類型有一個(gè)+運(yùn)算符)情況下都能工作良好。這個(gè)泛型缺點(diǎn)不是一個(gè)調(diào)試錯(cuò)誤或缺陷-它是設(shè)計(jì)造成的。泛型的實(shí)例化是在運(yùn)行時(shí)通過調(diào)用配件集實(shí)現(xiàn)的,因此編譯器不能確知一特定操作能被施行于一個(gè)泛型參數(shù),除非它匹配一個(gè)子類型約束,因此編譯器在定義泛型時(shí)解決這個(gè)問題。當(dāng)你使用泛型時(shí)的另外一個(gè)妨礙是,它不會(huì)允許你使用非類型參數(shù)。下列泛型類定義不會(huì)編譯:
generic<typename T, int x> ref class G{}; |
編譯錯(cuò):
error C2978: syntax error : expected ’typename’ or ’class’; found type ’int’; non-type parameters are not supported in generics |
與托管模板相比較:
template<typename T, int x = 0> ref class R{}; |
如果你開始感激C++向你提供了泛型和托管模板,那么請看下面這一個(gè)例子:
template<typename T> ref class R{ public: void F() { Console::WriteLine("hey"); } }; template<> ref class R<int> { public: void F() { Console::WriteLine("int"); } }; |
你不能用泛型這樣編碼;否則,將產(chǎn)生:
編譯錯(cuò):error C2979: explicit specializations are not supported in generics
但可以在繼承鏈中混合使用模板和泛型:
generic<typename T> ref class Base { public: void F1(T){} }; template<typename T> ref class Derived : Base<T> { public: void F2(T){} }; //... Derived<int> d; d.F1(10); d.F2(10); |
最后,你不能從一個(gè)泛型參數(shù)類型派生一個(gè)泛型類。
下列代碼不會(huì)成功編譯:
generic<typename T> ref class R : T {}; |
error C3234: a generic class may not derive from a generic type parameter
模板讓你這樣做(好像你還不知道這些):
ref class Base{ public: void F(){} }; generic<typename T> ref class R : T {}; //... R<Base> r1; r1.F(); |
這樣,當(dāng)你下次遇到對泛型的貶謗時(shí),你就知道該怎么做了。
6. STL/CLR
當(dāng)大量使用STL的C++開發(fā)者轉(zhuǎn)向.NET1/1.1時(shí)一定感覺非常別扭,他們中的許多可能會(huì)放棄并轉(zhuǎn)回到原來的本機(jī)編碼。從技術(shù)上講,你能結(jié)合.NET類型(using gcroot)使用本機(jī)STL,但是產(chǎn)生的結(jié)果代碼可能相當(dāng)?shù)托В挥谜f是丑陋了:
std::vector< gcroot<IntPtr> >* m_vec_hglobal; //... for each(gcroot<IntPtr> ptr in *m_vec_hglobal) { Marshal::FreeHGlobal(ptr);} |
大概VC++小組考慮到了這些并決定在Whidbey以后,他們會(huì)提供STL.NET(或STL/CLR)并可以單獨(dú)從網(wǎng)上下載。
你可能問為什么?Stan Lippman,在他的MSDN文章(STL.NET Primer)中給出了3條原因:
·可擴(kuò)展性--STL設(shè)計(jì)把算法和容器隔離到自己的應(yīng)用空間-也就是你可以有一組容器和一組算法,并且你能在任何一個(gè)容器上使用這些算法;同時(shí)你能在任何一個(gè)算法中使用這些容器。因此,如果你添加一種新的算法,你能在任何一種容器中使用它;同樣,一個(gè)新的容器也可以與現(xiàn)有算法配合使用。
·統(tǒng)一性--所有核心C++開發(fā)者集中在一起,匯集起他們精妙的STL專長,再使用他們的專長則輕車熟路。要較好地使用STL需要花費(fèi)時(shí)間-然而一旦你掌握了它,你就有了在.NET世界中使用你的技巧的明顯優(yōu)勢。不是嗎?
·性能--STL.NET通過使用實(shí)現(xiàn)泛型接口的托管模板實(shí)現(xiàn)。并且既然它的核心已用C++和托管模板編碼,可以期盼它比在BCL上使用的泛型容器更具有性能優(yōu)勢。
使用過STL的人不需要任何示范,所以下面代碼有益于以前沒有使用過STL的人。
vector<String^> vecstr; vecstr.push_back("wally"); vecstr.push_back("nish"); vecstr.push_back("smitha"); vecstr.push_back("nivi"); deque<String^> deqstr; deqstr.push_back("wally"); deqstr.push_back("nish"); deqstr.push_back("smitha"); deqstr.push_back("nivi"); |
我使用了兩個(gè)STL.NET容器-vector和deque,并裝滿兩個(gè)容器,使其看起來相同(在兩個(gè)容器中都使用了push_back)。現(xiàn)在,我將在兩個(gè)容器上使用replace算法-我們再次看到,這些代碼是很相同的。
replace(vecstr.begin(), vecstr.end(), gcnew String("nish"), gcnew String("jambo")); replace(deqstr.begin(), deqstr.end(), gcnew String("nish"), gcnew String("chris")); |
這里特別要注意的是我使用了"同樣"的算法--replace并在兩個(gè)不同STL容器上使用相同的函數(shù)調(diào)用。這是當(dāng)Stan談及"可擴(kuò)展性"時(shí)的意思。下面我用一個(gè)簡單函數(shù)來證明:
template<typename ForwardIterator> void Capitalize( ForwardIterator first,F(xiàn)orwardIterator end) { for(ForwardIterator it = first; it < end; it++) *it = (*it)->ToUpper(); } |
它遍歷一個(gè)System::String^容器并把其中的每個(gè)字符串轉(zhuǎn)化為大寫。
Capitalize(vecstr.begin(), vecstr.end()); Capitalize(deqstr.begin(), deqstr.end()); for(vector<String^>::iterator it = vecstr.begin(); it < vecstr.end(); it++) Console::WriteLine(*it); Console::WriteLine(); for(deque<String^>::iterator it = deqstr.begin(); it < deqstr.end(); it++) Console::WriteLine(*it); |
上面我的算法能夠與vector和deque容器工作良好。至此,不再細(xì)談;否則,guru站上的STL愛好者們會(huì)對我群起攻擊,而非STL人可能感到厭煩。如果你還沒使用過STL,可以參考有關(guān)資料。
7. 熟悉的語法
開發(fā)者經(jīng)常迷戀他們所用的編程語言,而很少是出于實(shí)用的目的。還記得當(dāng)微軟宣布不再為VB6提供官方支持時(shí),VB6人的反抗嗎?非VB6人對此可能非常震驚,而老道的VB6人早已為他們的語言作好葬禮準(zhǔn)備了。事實(shí)上,如果VB.NET從來沒被發(fā)明,多數(shù)VB6人將會(huì)離開.NET,因?yàn)镃#將會(huì)對他們非常陌生,而它的祖先就是C++。如今,許多VB.NET人可能已經(jīng)轉(zhuǎn)向了C#,但是他們不會(huì)從VB6直接轉(zhuǎn)向C#;VB.NET起到一個(gè)橋梁作用讓他們的思想脫離開原來VB6思想。相應(yīng)地,如果微軟僅發(fā)行VB.NET(而沒有C#),那么.NET可能成為了新的面向?qū)ο骎B,且?guī)в幸粋€(gè)更大的類庫-C++社團(tuán)的人可能對此嗤之以鼻-他們甚至不會(huì)麻煩地檢驗(yàn).NET基礎(chǔ)類庫。為什么任何使用一種特定語言的開發(fā)者會(huì)對另外一個(gè)團(tuán)體的使用另外開發(fā)語言的開發(fā)者嗤之以鼻?這不是我要回答的問題。--要回答該問題也許要先回答為什么有的人喜歡威士忌,有的人喜歡可口可樂,而還有人喜歡牛奶。所有我要說的是,對開發(fā)者來說,語法家族是個(gè)大問題。
你認(rèn)為對于一個(gè)具有C++背景的人,下面的代碼具有怎樣的直覺性?
char[] arr =new char[128]; |
他/她可能回答的第一件事是,方括號(hào)放錯(cuò)了位置。下面這句又如何?
"呀!"-最可能的反映。現(xiàn)在把下面與前面相比較:
char natarr[128]; array<char>^ refarr=gcnew array<char>(128); int y=refarr->Length; |
請注意聲明一個(gè)本機(jī)數(shù)組和一個(gè)托管數(shù)組時(shí)的語法區(qū)別。這里不同的模板形式的語法可視化地告誡開發(fā)者這一事實(shí)--refarr并不是典型的C++數(shù)組而且它可能是某種CLI類的派生物(事實(shí)上確是如此),所以極有可能可以把方法和屬性應(yīng)用于它。
C#的finalizer語法最有可能引起轉(zhuǎn)向C#的C++程序員的混淆。請看見下列C#代碼:
好,這樣~R看起來象一個(gè)析構(gòu)器但實(shí)際是個(gè)finalizer。為什么?請與下面的C++代碼比較:
ref class R { ~R(){ } !R(){ } }; |
這里~R是析構(gòu)器(實(shí)際上等價(jià)于一個(gè)析構(gòu)器的Dispose模式-但對C++人員來說,這它的行為象個(gè)析構(gòu)器)而新的!R語法是為finalizer建立的-這樣就不再有混淆而且語法看上去也與本機(jī)C++相匹配。
請看一下C#泛型語法:
再請看一下C++的語法:
generic<typename T> ref class R{}; |
曾經(jīng)使用過模板的人馬上就看出這種C++語法,而C#語法不能保證其沒有混淆性且也不直觀。我的觀點(diǎn)是,如果你以前具有C++背景,C++/CLI語法將最貼近于你以前所用。C#(以及J#)看上去象C++,但是還有相當(dāng)多的極為使人煩火的奇怪語義差別而且如果你沒有完全放棄C++,語法差別將永遠(yuǎn)不停地帶給你混亂和挫折。從這種意義上說,我認(rèn)為VB.NET更好些,至少它有自己唯一的語法,所以那些共用C++和VB.NET的人不會(huì)產(chǎn)生語法混亂。
8. 結(jié)論
最后,至于你用什么語言編程,這可能依賴許多因素——如:你在學(xué)校學(xué)習(xí)的是什么語言,你是用什么語言開發(fā)的現(xiàn)有代碼倉庫,是否你的客戶對你有具體的語言要求等。本文的主要目的是幫助你確定使用C++/CLI的幾個(gè)明確的場所--這里,它比另外CLI語言更具有明顯優(yōu)勢。如果你開發(fā)的應(yīng)用程序有90%的使用時(shí)間是涉及本機(jī)interop,為何還要考慮使用另外的而不是C++?如果你想開發(fā)一個(gè)通用集合,為什么僅把你自己限制在泛型上,而不是結(jié)合泛型和模板的優(yōu)勢呢?如果你已經(jīng)用C++工作,為什么還要學(xué)習(xí)一種新的語言?我常覺得象C#和VB.NET這樣的語言總是盡量向開發(fā)者隱藏CLR,而C++不僅讓你品味CLR,甚至可以讓你去親吻CLR!