依然热恋C++
Stanley B. Lippman 著 荣耀 译
摘要: 使用C++已多年的程序员现在正处于迷惑之中:他们的语言如何面对C#和微软.NET的来临?本文勾画了C++如何应用于.NET世界的路标。在.NET中,可以两种方式使用C++代码:托管(managed)和非托管(unmanaged)。非托管代码不使用CLR,而托管代码则致力于使用C++托管扩展。本文讨论了这两种方式。
我们这些C++社团里的人第一次感觉自己象是一个刚刚降临了一个新宝贝的家庭中年长的孩子,每一个人都狂热地围着这个新来的小家伙,此刻,即便能有人轻轻地拍一拍我们的脑袋,我们就很幸运了。我们很难不产生被忽视和一丁点儿伤害的感觉。在技术上这实际更糟糕,因为基础不断发生漂移,与之并进,事关生存。
当然,如今微软.NET框架是正在出售的新技术,多么丰盛的大餐啊!每一个人都对那个叫C#的语言啧啧不已。如果你是一名C++程序员,你很难不怀疑你是否应该学习C#,毕竟,在.NET讨论中,要不是拿它跟C#对比,难得提到一次C++。
C++程序员过时了吗?绝对不是!本文中,我将简介Visual Studio .NET当前版本中的新东西,并给你一些微软关于C++将来计划的想法。首先,我将简介Visual Studio.NET中C++的两个方面:标准C++和C++托管扩展。
保持兼容是标准C++最新进展背后的主要动机,也就是说,提供对ISO语言特性的支持。我还会讨论C++托管扩展,它使C++成为一种.NET语言。
Visual Studio .NET中的标准C++
标准兼容方面,已经在如下领域取得进展:
l 1.虚函数定义现在支持协变返回类型(covariant return type),对于编写类层次结构的人来说,这真是个喜讯。
l 2.静态整型常量成员现在可被显式初始化。
l 3.加入了对main中隐式返回0的支持。
下面,我将简要讨论每一个领域。
加入协变返回类型是对C++语言的第一个修改,已经标准委员会批准。这是奇妙的—派生类的虚函数现在可以返回一个派生类的实体,而它所重载的基类中的虚函数返回一个基类的实体。这是对类层次结构的一个重要的设计习语支持,它也是Visual Studio .NET中一个受欢迎的“附加物”。
这儿有一个典型用法的例子。考察抽象基类Query:
class Query
{
public:
virtual Query *clone() = 0;
//...
};
在派生类NotQuery的实体中,clone返回一个NotQuery对象的拷贝。例如:
class NotQuery //【译注:此类的声明有点问题】
{
public:
virtual ???? clone() {return new NotQuery(this);}
//...
public:
Query *operand;
};
假如没有对协变返回类型的支持,NotQuery实体clone的返回类型必须为Query*:
//没有协变返回类型支持...
virtual Query* clone() {return new NotQuery(this);}
这会工作得很好,如果你将clone的返回值赋给一个Query*指针,就象这样:
NotQuery::NotQuery(const NotQuery &rhs) {operand = rhs.operand->clone();}
但这样不成—当你希望显式将其赋给一个NotQuery*时,如下:
NotQuery nq(new NameQuery("Shakespeare"); //【译注:少了个“)”】。
//oops: 非法赋值...
NotQuery *pnq0 = nq->clone();
//ok: 需显式造型转换...
NotQuery *pnq1 = static_cast<NotQuery*>(nq->clone());
有了对协变返回类型的支持,你可以显式声明NotQuery的clone返回一个NotQuery*:
//有了协变返回类型支持...
virtual NotQuery* clone() {return new NotQuery(this);}
这就允许你以一种直觉的方式来使用clone,而无需显式造型转换:
//ok: 隐式转换...
Query *pq = nq->clone();
//ok: 无需转换...
NotQuery *pnq = nq->clone();
尽管静态整型常量成员的显式初始化并不象协变返回类型这样的语言特性来得那么紧要,但在类需要常量表达式的情况下,它提供了重大的设计便利,比如用于固定尺寸的数组定义中。例如:
class Buffer
{
static const int ms_buf_size = 1024;
char m_buffer[ms_buf_size];
};
静态整型常量成员是C++中唯一可在类定义中被显式初始化的类成员,从而使得该成员的值可被类中的其它成分使用,比如m_buffer的维数尺寸。否则,一般使用枚举作为符号常量代替。
main的返回值表示程序的退出状态。约定俗成,0表示程序成功退出。对于标准C++,没有显式返回值的main意味着编译器将在其尾部之前插入一行
return 0;
Visual Studio .NET中的Visual C++终于遵从了这个标准。
Visual Studio .NET中的Managed C++
Visual Studio .NET中C++托管扩展主要支持以下三个应用策略:
l 1.为非托管API提供托管.NET包装类,从而可将现存的C++类暴露给微软.NET平台。
l 2.允许你使用微软.NET类框架,并可与unmanaged C++混用。对框架来说有三个方面:核心语言支持,例如集合类和系统I/O;基础编程类,如对线程、网络套接字和正则表达式的支持;应用领域支持,例如XML、ASP.NET、Web Services、Windows Forms和ADO.NET等等。
l 3.你可以直接在.NET环境中编写,就好比在C#和Visual Basic中一样。不过,这个版本中的C++还未提供对RAD设计者的支持,比如Windows Forms和Web Forms。
首先,我将讨论包装一个非托管的实现。在我的《C++ Primer》第三版(Addison-Wesley, 1998)一书里,我创建了一个比较大的文本查询应用,它着重练习STL容器类,用以解析文本文件并建立内部表示。表1展示了包含文件,表2展示了数据表示。
表1 文本查询应用的包含文件 #include <algorithm> #include <string> #include <vector> #include <utility> #include <map> #include <set> #include <iostream> #include <fstream> #include <stddef.h> #include <ctype.h> using namespace std; |
表2 数据表示 typedef pair<short,short> location; typedef vector<location> loc; typedef vector<string> text; typedef pair<text*,loc*> text_loc; class TextQuery { public: // ... private: vector<string> *lines_of_text; text_loc *text_locations; map<string,loc*> *word_map; Query *query; static string filt_elems; vector<int> line_cnt; }; |
Query是一个解释查询语言的面向对象层次结构的抽象基类。表3展示了一个查询会话的可能运行方式。文本查询系统的一个本地调用看起来象下面这个样子:
int main()
{
TextQuery tq;
tq.build_up_text();
tq.query_text();
}
表3 查询会话 Enter a query-please separate each item by a space. Terminate query (or session) with a dot( . ). ==> fiery && ( bird || shyly ) fiery ( 1 ) lines match bird ( 1 ) lines match shyly ( 1 ) lines match ( bird || shyly ) ( 2 ) lines match fiery && ( bird || shyly ) ( 1 ) lines match Requested query: fiery && ( bird || shyly ) ( 3 ) like a fiery bird in flight. A beautiful fiery bird, he tells her, |
我希望无需做什么修修补补的事情(更不要说重新实现它了)就可将TextQuery接口暴露给.NET平台。毕竟,它能够工作。我心里没底—是不是如果我想移到.NET平台就意味着我必须放弃在本地代码上花费的功夫?幸运地是,将本地代码进行包装以暴露给.NET平台,是managed C++的拿手好戏。表4展示了干这种事的代码的可能模样。
表4 包装本地C++类 #include "TextQuery.h" __gc class TextQueryNet { private: TextQuery *pquery; public: TextQueryNet() : pquery(new TextQuery()){} ~TextQueryNet() {delete pquery;} void query_text() {pquery->query_text();} void build_up_text() {pquery->build_up_text();} // ... }; |
__gc修饰符(见表4所示)将该类标记为一个托管类—一个配置于CLR托管堆上的被垃圾收集的类。我将本地类声明为指针成员,利用表达式new将其配置在非托管堆上,就象我在本地代码里做的一样。因为它不被垃圾收集,我在析构器里将它delete掉。build_up_text和query_text都充当存根函数(stub functions),将调用派发到实际的TextQuery对象。
表5展示了修改过的main函数,它是由managed C++项目向导生成的。
表5 生成的main函数 #include "stdafx.h" #using <mscorlib.dll> #include <tchar.h> using namespace System; #include "gc_TextQuery.h" int _tmain(void) { Console::WriteLine(S"Beginning managed wrapper test ..."); TextQueryNet *tqn = new TextQueryNet(); tqn->build_up_text(); tqn->query_text(); Console::WriteLine(S"Ending managed wrapper test ..."); return 0; } |
现在让我们来讨论如何使用.NET框架。假如有人问我ISO标准C++最严重的缺点是什么,我会说是这样的一个事实:没有诸如线程、网络编程、正则表达式和XML等编程领域的标准库。标准委员会计划在下一轮时间里弥补这个不足,但离现在还有几年,这期间你怎么办?幸运地是,.NET框架提供了一个颇具吸引力的解决方案。例如,表6展示了一个使用了.NET框架的简单套接字服务器类。它接受对Northwind的employees的电话号码查询,Northwind是一个同Visual Studio .NET一起分发的样例SQL数据库。
表6 简单套接字服务器类 //包含必要的装配件(assemblies) #using <mscorlib.dll> #using <System.dll> #using <System.Data.dll> #using <System.Xml.dll> //打开相关名字空间 using namespace System; using namespace System::Threading; using namespace System::Data; using namespace System::Data::SqlClient; using namespace System::Collections; using namespace System::Net::Sockets; //ok: 这儿是我们的类 __gc class SocketDemo_Server { private: static const int port = 4554; static const int maxPacket = 128; TcpListener *tcpl; DataSet *ds; DataRowCollection *rows; public: SocketDemo_Server(); void Start(); void handleConnection(); //grab the data from the SQL database void retrieveData(); }; |
表7是运行这个服务器并记录其处理三个客户请求的示例输出。表8则展示了表6中所示的Start函数的实现。Start函数使用Thread类生成独立线程,以从数据库中取得数据并处理客户连接。因为WriteLine需要一个引用型的参数,因此必须将port值显式装箱(box)。
表7 服务器输出 Server[4554]: OK: started TcpListener ... Server[4554]: OK: listening for connections ... Server[4554]: OK: retrieved SQL database info ...
Server[4554]: OK: a client connected ... Server[4554]: OK: client requested phone # for Fuller Server[4554]: OK: first request for Fuller Server[4554]: Phone number for Fuller: (206) 555-9482
Server[4554]: OK: a client connected ... Server[4554]: OK: client requested phone # for King Server[4554]: OK: first request for King Server[4554]: Phone number for King: (71) 555-5598
Server[4554]: OK: a client connected ... Server[4554]: OK: client requested phone # for Fuller Server[4554]: OK: cached request for Fuller Server[4554]: Phone number for Fuller: (206) 555-9482
Server[4554]: OK: a client connected ... Server[4554]: OK: client requested phone # for Musil Server[4554]: OK: first request for Musil Server[4554]: Phone number for Musil: Sorry. Cannot be found. |
表8 Start函数 void SocketDemo_Server::Start() { try { tcpl = new TcpListener(port); tcpl->Start(); Console::WriteLine(S"Server[{0}]: OK: started TcpListener ...", __box( port )); //从数据库中取得数据 ... Thread *tdata = new Thread(new ThreadStart(this, &SocketDemo_Server::retrieveData)); tdata->Start(); //ok: 蹬掉线程 ... //处理socket连接的线程 ... Thread *tconnect = new Thread(new ThreadStart(this, &SocketDemo_Server::handleConnection)); tconnect->Start(); } catch(Exception *ex) { Console::WriteLine(S"Oops: Unable to Set Up SocketDemo_Server"); Console::WriteLine(ex->ToString()); } } |
类Exception是.NET异常层次结构的根。ToString方法显示了一个完整的栈跟踪,这酷毙了。ThreadStart是一个委托类型—一种既可指向静态也可指向非静态成员函数的泛型指针。
我可以进一步探究这个实现,但我认为你已经能够得到一个对框架威力和易用性的感性认识了。更为重要的是,你可以看到,通过C++来使用它是多么得easyJ
C++的将来
希望在读完这篇关于Visual Studio .NET中的C++的短文后,能够使你打消疑虑、重树信心。Visual C++不但仍是这个家庭中成员之一,并且它还是一个重要的家庭成员!为了突出这个重要性,微软Visual C++小组正努力工作于一个过渡版本,争取尽快交付这些特性。谈及ISO标准C++,这个小组已经以异乎寻常的步幅进行兼容性工作。对于那些牛气的程序员,这意味着模板、模板、模板。大量使用了模板的第三方库,比如Loki和Boost,现在可以在内部编译而不需要兜什么圈子了。正如好莱坞所言:别走开。我们不是啥都还没看见吗!
-全文完-