winston 发表于 2011-12-17 19:42:10

C++/CLI:第一流的CLI语言

1. 简介

  本文并不是为了奉承C++/CLI的辉煌,也不是为了贬低其它如C#或者VB.NET等语言,相反,这只是一个非官方的、以一个喜欢这种语言的非微软雇员身份来论证C++/CLI有它的自己的唯一的角色,可作为第一流的.NET编程语言。

  一个不断在新闻组和技术论坛上出现的问题是,当象C#和VB.NET这样的语言更适合于这种用途时,为什么要使用C++来开发.NET应用软件。通常这样一些问题后面的评论说是,C++语法是怎样的复杂和令人费解,C++现在是怎样一种过时的语言,还有什么VS.NET设计者已不再像支持C#和VB.NET一样继续支持C++。其中一些猜疑是完全荒谬的,但有些说法部分正确。希望本文有助于澄清所有这些围绕C++/CLI语言及其在VS.NET语言层次中的地位的疑惑,神秘和不信任。请记住,本作者既不为微软工作也没有从微软那里取得报酬,只是想从技术上对C++/CLI作一评判。

  2. 快速简洁的本机interop

  除了P/Invoke机制可用在另外的象C#或VB.NET这样的语言外,C++提供了一种独有的interop机制,称作C++ interop。C++ interop比P/Invoke直观得多,因为你只是简单地#include需要的头文件,并与需要的库进行链接就能象在本机C++中一样调用任何函数。另外,它比P/Invoke速度快--这是很容易能证明的。现在,可争辩的是在实际应用软件的开发中,经由C++ interop获得的性能好处与花在用户接口交互、数据库存取、网络数据转储、复杂数学算法等方面的时间相比可以被忽略,但是事实是在有些情况下,甚至通过每次interop调用节省的几个纳秒也能给全局应用程序性能/响应造成巨大影响,这是绝对不能被忽视的。下面有两部分代码片断(一个是使用P/Invoke机制的C#程序,一个是使用C++ Interop机制的C++程序),我分别记录了其各自代码重复执行消耗的时间(毫秒)。不管你如何解释这些数据,不管这会对你的应用程序产生什么影响,全是你的事。我仅打算事实性地指出,C++代码的执行速度要比C#(其中使用了较多的本机interop调用)快。

  1) C#程序(使用P/Invoke)




static extern uint GetTickCount();


static extern uint GetWindowsDirectory(
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;
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) 速度比较


重复次数 C# 程序C++程序
50,00061 10
500,000600 70
1,000,0001162140
5,000,0006369721
  
  其性能差别真是令人惊愕!这的确是说明为什么要使用C++/CLI的一个好理由,如果你在使用本机interop进行开发,那么性能!完全由于性能,我就将被迫借助本机interop来实现并非基于web的.NET应用程序。当然,为什么我想要使用.NET来开发需要大量本机interop技术的应用程序完全是另外一个问题。

  如果你仍怀疑这种性能优势,有另外的理由来说明你为什么不得不使用C++/CLI而不是C#或VB.NET——源码膨胀!下面是一个C++函数的例子,它使用了IP帮助者API来枚举一台机器上的网络适配器并且列出与每个适配器相联系的所有IP地址。

  4) 枚举n/w适配器的C++代码


void ShowAdapInfo()
{
PIP_ADAPTER_INFO pAdapterInfo = NULL;
ULONG OutBufLen = 0;
//得到需要的缓冲区大小
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;
//取得适配器信息
if( GetAdaptersInfo(pAdapterInfo, &OutBufLen) != ERROR_SUCCESS )
{//调用失败 }
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;
}
}

  现在让我们看一个使用P/Invoke的C#版本。

  5) 使用P/Invoke技术的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;

public struct IP_ADDRESS_STRING
{

public string Address;
}

public struct IP_ADDR_STRING
{
public IntPtr Next;
public IP_ADDRESS_STRING IpAddress;
public IP_ADDRESS_STRING Mask;
public Int32 Context;
}

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;
}

public static extern int GetAdaptersInfo(IntPtr pAdapterInfo, ref int pBufOutLen);
static void ShowAdapInfo()
{
int OutBufLen = 0;
//得到需要的缓冲区大小
if( GetAdaptersInfo(IntPtr.Zero, ref OutBufLen) ==
ERROR_BUFFER_OVERFLOW )
{
IntPtr pAdapterInfo = Marshal.AllocHGlobal(OutBufLen);
//取得适配器信息
if( GetAdaptersInfo(pAdapterInfo, ref OutBufLen) != ERROR_SUCCESS )
{ //调用失败了 }
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. 栈语义和确定性的析构

  C++经由栈语义模仿给了我们确定性的析构。简言之,栈语义是Dispose模式的良好的语法替代品。但是它在语义上比C# using块语法更直观些。请看下列的C#和C++代码段(都做一样的事情-连接两个文件的内容并把它写到第三个文件中)。

  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块语法让程序员自己明确地指定他想在哪儿调用Dispose(using块的结束处),而使用C++/CLI的栈语义,只需让编译器使用常规的范围规则来处理它即可。事实上,这使得在C#中修改代码比在C++中更乏味-作为一实例,让我们修改这些代码以便即使仅存在一个输入文件也能创建输出文件。请看下面修改后的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块放到顶层需要重新调整using块结构--这在上面情况下显然不是个大问题,但是对于实际开发中的修改,这可能是相当模糊的且易导致逻辑错误的。

  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声明移到了顶部并增加了一个额外的try块,就这些。甚至对于象在我的示例代码片断中的琐碎事情,如果所涉及的复杂性在C++中大大减少,那么,当你工作于更大的工程时你能想象使用栈语义对你的编码效率千万的影响。

  还不确信?好,让我们看一下成员对象和它们的析构吧。Imagine CLI GC类R1和R2,二者都实现了Idisposable接口且都有函数F(),还有一个CLI GC类R,它有R1和R2成员和一个函数F()-它内部地调用R1和R2上的F()成员函数。让我们先看C#实现。

  5) 一个disposable类继承层次的C#实现


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();}
}
}

  这里有几件事情要做:必须为每个disposable 类手工实现IDisposable接口,对于具有成员R1和R2的类R,Dispose方法也需要调用成员类上的Dispose。现在让我们分析上面几个类的C++实现。

  6) 等价的C++实现


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接口实现(我们的类中仅建立了析构器)而且最好的部分--类R的析构器(Dispose方法)并没有在该类可能含有的可释放的成员上调用Dispose-它没有必要这样做,编译器自动为之生成所有的代码!

4. 混合类型

  我们知道,C++支持本机类型-总是如此;C++支持CLI类型-本文正是特别强调这一点;它还支持混合类型-具有CLI成员的本机类型和具有本机成员的CLI类型!请尽管考虑所有你能的可能需求。

  注意,谈到Whidbey,混合类型实现还不完整;就我从Brandon,Herb和Ronald发表的材料的理解得知,存在这种相当酷的类型--统一模型,它将在Orcas中实现--你能够在本机C++堆上new/delete CLI类型,而且也能够在CLI堆上gcnew/delete本机类型。但既然这是Whidbey以后的东西,本文不讨论统一模型。

  在我谈论你何时使用混合类型以前,我想向你说明什么是混合类型。如果你理解混合类型,请跳过下面几段。这里引用Brandon Bray的说法:"一种混合类型,或者是本机类ref类(需要有对象成员),或者是通过声明或继承被分配在垃圾回收堆或本机堆上的。"因此如果你有一个托管类型或者有一个有托管成员的本机类型,你就有了一个混合类型。VC++ Whidbey不直接支持混合类型(统一类型模型是一种Whidbey之后的概念),但是它给我们划定了实现混合类型的条件。让我们开始讨论包含托管成员的本机类型。


ref class R
{
public:
void F(){}
//假定 non-trivial ctor/dtor
R(){}
~R(){}
};

  在我的例子中,设想该托管类型R有一个non-trivial构造器和一个non-trivial析构器。


class Native
{
private:
gcroot<R^> m_ref;
public:
Native():
m_ref(gcnew R()){}
~Native()
{ delete m_ref; }
void DoF()
{ m_ref->F(); }
};

  既然,我不能在我的类中拥有一个R成员,我使用了gcroot模板类(在gcroot.h中声明,但是你要用"#include vcclr.h"),它包装了System::Runtime::InteropServices::GCHandle结构。它是个象类一样的灵敏指针,它重载了运算符->以返回用作模板参数的托管类型。因此在上面类中,我可以使用m_ref,就好象我已经声明它是R^,而且你能在DoF函数中看到这正在起作用。实际上你可以节省delete,这可以通过使用auto_gcroot(类似于std::auto_ptr,在msclr\auto_gcroot.h文件中声明)代替gcroot来实现。下面是一个更好些的使用auto_gcroot的实现。


class NativeEx
{
private:
msclr::auto_gcroot<R^> m_ref;
public:
NativeEx() : m_ref(gcnew R()){}
void DoF()
{ m_ref->F(); }
};

  下面让我们看相反的情形:一个CLI类的本机成员。


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(); }
};

  我不能定义一个Native对象来作为一个ref类成员,因此需要使用一个Native*对象来代替。我在构造器中new该Native对象,然后在析构器和finalizer中delete它。如果你运行该工程的调试版,在执行到finalizer时将抛出一个异常- 因此开发者可以马上添加一个对delete的调用或为他的CLI类型使用栈语义技术。奇怪的是,库开发小组没有建立一个gcroot的反向实现-但这不是个大问题,我们可以自己写。


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
}
};

  这仅是个相当简单的灵敏指针实现,就象一个负责本机对象分配/回收的ref类。不管怎样,借助nativeroot模板类,我们可以如下修改托管类:


ref class ManagedEx
{
private:
nativeroot<Native> m_nat;
public:
void DoF()
{ m_nat->DoF(); }
};

  好,关于混合类型的最大问题是什么呢?你可能问。最大问题是,现在你能混合使用你的MFC、ATL、WTL、STL代码仓库和.NET框架,并用可能的最直接的方式-只需写你的混合模式代码并编译实现!你可以建立在一个DLL库中建立MFC 类,然后建立一个.NET应用程序来调用这个DLL,还需要把.NET类成员添加到你的MFC类(也实现可以相反的情况)。

  作为一例,设想你有一MFC对话框--它通过一个多行的编辑框接受来自用户的数据-现在,你有一新的要求-显示一个只读编辑框,它将显示当前在该多行编辑框中文本的md5哈希结果。你的队友正在悲叹他们将必须花费几个小时钻研crypto API,而你的上司在担忧你们可能必须要买一个第三方加密库;那正是你在他们面前树立形象的时候,你宣布你将在15分钟内做完这项任务。下面是解决的办法:

  添加一个新的编辑框到你的对话框资源中,并且添加相应的DDX变量。选择/clr编译模式并且添加下列代码到你的对话框的头文件中:


#include <msclr\auto_gcroot.h>
using namespace System::Security::Cryptography;

  使用auto_gcroot模板来声明一个MD5CryptoServiceProvider成员:


protected:
msclr::auto_gcroot<MD5CryptoServiceProvider^> md5;

  在OnInitDialog过程中,gcnew MD5CryptoServiceProvider成员。


md5 = gcnew MD5CryptoServiceProvider();

  并且为多行编辑框添加一个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 = static_cast<Byte>(str);
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);
}

  这里使用了混合类型:一个本机Cdialog派生类,该类含有一个MD5CryptoServiceProvider成员(CLI类型)。你可以轻易地试验相反的情况(如早期的代码片断已显示的)——可以建立一个Windows表单应用程序而且可能想利用一个本机类库--这不成问题,使用上面定义的模板nativeroot即可。

5. 托管模板

  也许你对泛型的概念已很清楚了,它帮助你避免进入C++的模板梦魇,它是实现模板的最佳方式,等等。好,假设这些全部正确,C++/CLI支持泛型就象任何其它CLI语言一样-但是它有而其它一些CLI语言还没有的是它还支持托管模板-也就是模板化的ref和value类。如果你以前从未使用过模板,你不能一下欣赏这么多优点,但是如果你有模板使用背景而且你已发现了泛型中存在的可能限制你编码的方式,托管模板将会大大减轻你的负担。你能联合使用泛型和模板- 事实上有可能用一个托管类型的模板参数来实例化一个泛型类型(尽管相反的情形是不可能的,因为运行时刻实例化由泛型所用)。STL.NET (或STL/CLR)以后讨论,请很好地利用泛型和托管模板的混合编程吧。

  泛型使用的子类型约束机制将防止你写出下面的代码:


generic<typename T> T Add(T t1, T t2)
{ return t1 + t2; }

  编译错误:


error C2676: binary ’+’ : ’T’ does not define this operator or a conversion to a type acceptable to the predefined operator

  现在请看相应的模板版本:


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);

  这在一个象int的本机类型以及一个ref类型(只要ref类型有一个+运算符)情况下都能工作良好。这个泛型缺点不是一个调试错误或缺陷-它是设计造成的。泛型的实例化是在运行时通过调用配件集实现的,因此编译器不能确知一特定操作能被施行于一个泛型参数,除非它匹配一个子类型约束,因此编译器在定义泛型时解决这个问题。当你使用泛型时的另外一个妨碍是,它不会允许你使用非类型参数。下列泛型类定义不会编译:


generic<typename T, int x> ref class G{};

  编译错:


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++向你提供了泛型和托管模板,那么请看下面这一个例子:


template<typename T> ref class R{
public:
void F()
{ Console::WriteLine("hey"); }
};
template<> ref class R<int>
{
public:
void F()
{ Console::WriteLine("int"); }
};

  你不能用泛型这样编码;否则,将产生:

  编译错: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);

  最后,你不能从一个泛型参数类型派生一个泛型类。

  下列代码不会成功编译:


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();

  这样,当你下次遇到对泛型的贬谤时,你就知道该怎么做了。

  6. STL/CLR

  当大量使用STL的C++开发者转向.NET1/1.1时一定感觉非常别扭,他们中的许多可能会放弃并转回到原来的本机编码。从技术上讲,你能结合.NET类型(using gcroot)使用本机STL,但是产生的结果代码可能相当低效,更不用说是丑陋了:


std::vector< gcroot<IntPtr> >* m_vec_hglobal;
//...
for each(gcroot<IntPtr> ptr in *m_vec_hglobal)
{ Marshal::FreeHGlobal(ptr);}

  大概VC++小组考虑到了这些并决定在Whidbey以后,他们会提供STL.NET(或STL/CLR)并可以单独从网上下载。

  你可能问为什么?Stan Lippman,在他的MSDN文章(STL.NET Primer)中给出了3条原因:

  ·可扩展性--STL设计把算法和容器隔离到自己的应用空间-也就是你可以有一组容器和一组算法,并且你能在任何一个容器上使用这些算法;同时你能在任何一个算法中使用这些容器。因此,如果你添加一种新的算法,你能在任何一种容器中使用它;同样,一个新的容器也可以与现有算法配合使用。

  ·统一性--所有核心C++开发者集中在一起,汇集起他们精妙的STL专长,再使用他们的专长则轻车熟路。要较好地使用STL需要花费时间-然而一旦你掌握了它,你就有了在.NET世界中使用你的技巧的明显优势。不是吗?

  ·性能--STL.NET通过使用实现泛型接口的托管模板实现。并且既然它的核心已用C++和托管模板编码,可以期盼它比在BCL上使用的泛型容器更具有性能优势。

  使用过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");

  我使用了两个STL.NET容器-vector和deque,并装满两个容器,使其看起来相同(在两个容器中都使用了push_back)。现在,我将在两个容器上使用replace算法-我们再次看到,这些代码是很相同的。


replace(vecstr.begin(), vecstr.end(),
gcnew String("nish"), gcnew String("jambo"));
replace(deqstr.begin(), deqstr.end(),
gcnew String("nish"), gcnew String("chris"));

  这里特别要注意的是我使用了"同样"的算法--replace并在两个不同STL容器上使用相同的函数调用。这是当Stan谈及"可扩展性"时的意思。下面我用一个简单函数来证明:


template<typename ForwardIterator> void Capitalize(
ForwardIterator first,ForwardIterator end)
{
for(ForwardIterator it = first; it < end; it++)
*it = (*it)->ToUpper();
}

  它遍历一个System::String^容器并把其中的每个字符串转化为大写。


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容器工作良好。至此,不再细谈;否则,guru站上的STL爱好者们会对我群起攻击,而非STL人可能感到厌烦。如果你还没使用过STL,可以参考有关资料。

7. 熟悉的语法

  开发者经常迷恋他们所用的编程语言,而很少是出于实用的目的。还记得当微软宣布不再为VB6提供官方支持时,VB6人的反抗吗?非VB6人对此可能非常震惊,而老道的VB6人早已为他们的语言作好葬礼准备了。事实上,如果VB.NET从来没被发明,多数VB6人将会离开.NET,因为C#将会对他们非常陌生,而它的祖先就是C++。如今,许多VB.NET人可能已经转向了C#,但是他们不会从VB6直接转向C#;VB.NET起到一个桥梁作用让他们的思想脱离开原来VB6思想。相应地,如果微软仅发行VB.NET(而没有C#),那么.NET可能成为了新的面向对象VB,且带有一个更大的类库-C++社团的人可能对此嗤之以鼻-他们甚至不会麻烦地检验.NET基础类库。为什么任何使用一种特定语言的开发者会对另外一个团体的使用另外开发语言的开发者嗤之以鼻?这不是我要回答的问题。--要回答该问题也许要先回答为什么有的人喜欢威士忌,有的人喜欢可口可乐,而还有人喜欢牛奶。所有我要说的是,对开发者来说,语法家族是个大问题。

  你认为对于一个具有C++背景的人,下面的代码具有怎样的直觉性?


char[] arr =new char;

  他/她可能回答的第一件事是,方括号放错了位置。下面这句又如何?


int y=arr.Length;

  "呀!"-最可能的反映。现在把下面与前面相比较:


char natarr;
array<char>^ refarr=gcnew array<char>(128);
int y=refarr->Length;

  请注意声明一个本机数组和一个托管数组时的语法区别。这里不同的模板形式的语法可视化地告诫开发者这一事实--refarr并不是典型的C++数组而且它可能是某种CLI类的派生物(事实上确是如此),所以极有可能可以把方法和属性应用于它。

  C#的finalizer语法最有可能引起转向C#的C++程序员的混淆。请看见下列C#代码:


class R
{ ~R(){} }

  好,这样~R看起来象一个析构器但实际是个finalizer。为什么?请与下面的C++代码比较:


ref class R
{
~R(){ }
!R(){ }
};

  这里~R是析构器(实际上等价于一个析构器的Dispose模式-但对C++人员来说,这它的行为象个析构器)而新的!R语法是为finalizer建立的-这样就不再有混淆而且语法看上去也与本机C++相匹配。

  请看一下C#泛型语法:


class R<T>{};

  再请看一下C++的语法:


generic<typename T> ref class R{};

  曾经使用过模板的人马上就看出这种C++语法,而C#语法不能保证其没有混淆性且也不直观。我的观点是,如果你以前具有C++背景,C++/CLI语法将最贴近于你以前所用。C#(以及J#)看上去象C++,但是还有相当多的极为使人烦火的奇怪语义差别而且如果你没有完全放弃C++,语法差别将永远不停地带给你混乱和挫折。从这种意义上说,我认为VB.NET更好些,至少它有自己唯一的语法,所以那些共用C++和VB.NET的人不会产生语法混乱。

  8. 结论

  最后,至于你用什么语言编程,这可能依赖许多因素——如:你在学校学习的是什么语言,你是用什么语言开发的现有代码仓库,是否你的客户对你有具体的语言要求等。本文的主要目的是帮助你确定使用C++/CLI的几个明确的场所--这里,它比另外CLI语言更具有明显优势。如果你开发的应用程序有90%的使用时间是涉及本机interop,为何还要考虑使用另外的而不是C++?如果你想开发一个通用集合,为什么仅把你自己限制在泛型上,而不是结合泛型和模板的优势呢?如果你已经用C++工作,为什么还要学习一种新的语言?我常觉得象C#和VB.NET这样的语言总是尽量向开发者隐藏CLR,而C++不仅让你品味CLR,甚至可以让你去亲吻CLR!
页: [1]
查看完整版本: C++/CLI:第一流的CLI语言