ANSI/ISO C++ Professional Programmer's Handbook(14)

摘自:http://sttony.blogspot.com/search/label/C%2B%2B

14


总结和未来的方向


by Danny Kalev




"Hey, we're done!"



(这是1997年11月在New Jersey,Morristown举行的的国际标准化组织ANSI/ISO C++委员会批准C++最后草案的会议之后Josee Lajoie的话。)


在前面的章节讲述了C++的过去和现在。在最近20 年,C++ 从一种试验性语言演化成了在世界范围内广泛使用的面向对象语言。C++标准化的重要性不言而喻。有ANSI/ISO 的认可有许多优点:




  • 语言稳定性——当今C++ 大概是在商业应用中使用最广泛的语言。从空白学习它是一个苛刻耗时的过程。之后它保证学习C++是一个一次投资,不需要反复学习。





  • 代码稳定性——标准指定了一套反对的特性,在未来这些特性变成陈腐的东西。而完全符合标准的代码在未来保证可以工作。





  • 良好的适应性——C++程序员可以轻松的转换环境,工程,编译器和小组。





  • 简单的移植——标准定义了对于所有平台和编译器厂商都通用的部分,使得跨越操作系统和硬件的软件移植很容易。





下面的代码是符合标准的一个例子;但是有些编译器会拒绝它,而有些编译器可以成功编译:



#include <iostream>
using namespace std;
void detect_int(size_t size)
{
switch(size)
{
case sizeof(char):
cout<<"char detected"<<endl;
break;
case sizeof(short):
cout<<"short detected"<<endl;
break;
case sizeof(int):
cout<<"int detected"<<endl;
break;
case sizeof(long):
cout<<"int detected"<<endl;
break;
}
}

在对所有四种类型都用不同大小表示的平台上(例如用16比特表示short,用32比特表示int,用64比特表示long),这些代码可以编译并工作正常。在那些int与其他整数类型大小一样的平台上,编译器将报告一样的case条件。


从这个例子可以看出,标准并不保证绝对的可移植性,它也不保证二进制的兼容性。但是,通过定义语言的公共基础来简化跨平台移植,这些公共基础允许编译器扩展。这些习惯几乎是通用的:平台相关的库和关键字被添加到每一个C++编译器中。但是,编译器不能改变标准的规范(否则,这样的编译器就不是遵循标准的)。就像你在下面一节中读到的,允许平台相关的扩展对于一种通用语言的成功是十分重要的;禁止平台相关扩展的语言将因缺乏厂商支持而失去用户。


本章的范围


前面的章节都在探讨C++怎么样,本章将探讨为什么。它阐述了C++的设计和演化底下的哲学观念,并且与其他语言的演化进行比较。许多差一点要进标准的特性都将展示出来。可能附加到C++的特性,包括自动垃圾收集、对象持久化和并发性,都将被讨论。最后,将讨论一些理论性和试验性的问题。讨论的意图并不是想预言C++的特性(并不能保证讨论过的特性在什么时候将变成标准的一部分),只是向你展示设计语言方面的视野。


一些差一点被加入到标准的特性


C++的标准化持续了九年。STL在最后一年才加入到议程中。但是,STL是一个例外。其他提出过迟的特性都没有包括在标准中。下面的章节列出两种这种特性:hashed关联容器和函数模板的默认类型。


Hashed联合容器


标准模板库值提供了一种类型的关联容器——sorted 关联容器。STL的sorted关联容器是mapmultimapsetmultiset(参见第十章,“STL和泛型程序设计”)。但是另外一种类型的关联容器,hashed关联容器将要在标准库中了,只是它提出的太晚,没有加入标准。sorted 关联容器和hashed 关联容器的区别是创建者保存根据总数排序的关键字。例如,在一个 map<string, int>中,元素根据字符的字典顺序排序。而一个hashed关联容器根据数字子集划分关键字,每一个关键字通过一个hash函数来产生。因此,搜索一个关键字限制在它的子集中,而不是整个关键字空间。在一些情况下搜索一个hashed 关联容器可以比搜索sorted关联容器要快;但是与sorted关联容器不一样,性能可以预言。已经有厂商包括hashed关联容器作为扩展,而且这个容器很有可能增加到标准的下一个版本。


函数模板的默认类型参数


就像你在第九章“模板”读到的,类模板可以接受默认类型参数。但是,标准不允许函数模板的默认类型参数。这个在类模板和函数模板之间的区别被认为是一个发现的太迟的简单的疏忽。函数模板的默认类型参数非常有可能加入下一个版本的标准。



C++演化过程与其他语言的比较


与其他新语言不同,C++ 不是一个商业公司的产品。C++不能忍受商标,它的创建者也不从每一个编译器抽版税。因此,C++不是一个市场营销的产物,也不是软件公司之间商业战争的产物。其他区别C++和"would-be perfect"程序设计语言的重要因素是它已经被设计和扩展了很多年了。


有些人也许还记得早些时候关于Ada的极大宣传。Ada或许是当时最傲慢的想弥补其他语言不足的一种语言。 Ada许诺将是一种100%可移植语言,没有任何子集和方言(subsets and dialects)。它也内建对多任务和参数化类型的支持。Ada的设计持续了进10年,但是它是一个design by committee过程而不是象C++一样是design by community的过程。大家都知道的事实是:Ada 从没有象它宣称的成为一种通用目的,广泛使用的程序设计语言。现在回想起来有趣的是,在1983年Ada语言发布的时候,许多人相信它是创建的最后一种第三代语言。具有讽刺意味的是,就在同时C++开始了它的第一步。更不要说,C++的设计和演化有一条更本不同的道路。其他第三代语言从1983年开始陆续出现而且——的确是这样——新的第三代语言将在将来继续出现。导致Ada没有成为通用语言的原因可以作为一个设计语言的教训。


用户的利益


Ada的失败主要可以归结于由委员会着手设计。另外,禁止平台相关的扩展阻止了厂商开发新语言支持的库和工具。了解计算机科学家和语言用户对待语言重要特性的观点是如何不同,总是令人惊讶的。由程序员而不是科学家创建的C语言,提供了方便高效的可读性和安全性。例如,写下面这种语句的能力



if (n = v) //程序员将等于写成了赋值?
{
//...do something
}

成为批评的源头。但是,就是这个特性允许程序员写由一个语句组成的完整函数,象下面这样:



void strcpy (char * dst, const char * src)
{
while( *dst++ = *src++ );
}

输入长关键字的麻烦也是争论的问题。“理论语言(Academic languages)”经常鼓吹使用冗长的语句来组成完整的关键字——例如,integer而不是intcharacter而不是char(就像在Eiffel和其他类似语言中那样),使用call func();而不是func();。另一方面程序员更喜欢简短的关键字和符号。参见下面:



class Derived : Base {}; //继承符号是:

在其他语言中,继承用显式的关键字表示



class Derived extends Base {}; //Java;完整关键字来指示继承

在这方面C++采用了 C的方针。此外,根据Bjarne Stroustrup的说法,C++设计的一个原则是,如果可以选择麻烦写编译器作者还是麻烦程序员的话,选择麻烦编译器作者(The Evolution of C++: Language Design in the Marketplace of Ideas, p.50)。实现运算符重载,枚举类型,模板,默认参数和Koenig查找都是这个方法的例子。不要语言的直接支持程序员也可以使用这些特性:可以使用普通函数可以代替重载运算符,常量可以代替enum类型,没有Koenig 查找也可以用完全名字。幸运的是,在C++中这些都由编译器完成了。但是在其他语言中采用相反的策略,也就是简单编译器作者而麻烦程序员。例如在Java中没有枚举类型,运算符重载和默认参数。尽管这些特性不会带来任何额外开销,而他们的重要性和用处是毫无疑问的,他们使得编译器作者的工作更加麻烦(起初,Java 的设计者宣称运算符重载增加不必要的复杂性)。


"Pay as You Go"


面向对象程序设计的好处不是免费的。自动调用构造器和销毁器是非常简单的,但是它产生在速度和程序大小上的额外开销。同样的,动态绑定和虚继承也影响性能。但是这些特性不会强加给程序员。纯过程化C++代码(例如移植到C++编译器的就旧C代码)就不需要为这些特性花费任何东西。换句话说,用户——几乎没有意外——在降低性能的高级特性,和易受设计改变影响难以维护的低级特性之间可以选择。"pay as you go"原则使的程序员能在不同的应用领域使用C++,而且可以根据需要和优先权来使用不同 的程序设计范例。


将来可能加入C++的特性


很难预测将来由那些特性被加入到C++中,主要是因为很难预测将来5到10年之内通用程序设计将发生什么变化。但是,自动垃圾收集器,并发和对象持久化已经在许多面向对象语言中实现了;在未来,他们也可能加入C++。基于规则的程序设计和更好的对动态连接库的支持是另外两种最可能加入C++的特性。下面的章节讨论这些特性,他们极大的增加了复杂性。


自动垃圾收集器


“如果方便程序员是重要的,那么为什么C++没有垃圾收集器呢?”,这是一个经常提到的问题(垃圾收集器已在第十一章“内存管理”中讨论过)。显然,自动的垃圾收集器可以使程序员的工作更轻松。但是与对象、虚成员函数、动态转换不同,程序员没有选择垃圾收集器的权利。如果垃圾收集器是一个对于程序员透明的自动过程,它违反了"pay as you go"原则。自动垃圾收集器是强加给用户的,即使程序员喜欢手工管理内存。


可以为自动垃圾收集器增加一个编译开关(就像许多编译器提供关闭RTTI的选项一样)?这是一个有趣的问题。的确在没有使用动态内存管理的程序里,程序员可能想关闭垃圾收集器。关键在于动态分配内存的程序。考虑下面的例子:



void f()
{
int * p = new int;
//...使用p
}

当垃圾收集器打开时,如果f()退出,编译器标记p为一个unreferenced的指针。因此,在垃圾收集器下一次调用中,p指向的内存将被释放。可是为这样的简单例子增加垃圾收集器过于麻烦。程序员key9i 使用auto_ptrauto_ptr在第六章“异常处理”和第十一章中讨论)来达到相同的效果。例如



void f()
{
auto_ptr<int> p (new int);
//...使用p
} //auto_ptr的销毁器释放呢p

当必须在一个范围释放另一个范围内分配的内存时,垃圾收集器是很有用的。例如,虚构造器(在第四章“特殊成员函数:默认构造器,拷贝构造器,销毁器和分配运算符特殊成员函数:默认构造器,拷贝构造器,销毁器和赋值运算符”)可以使用户在不知道源对象具体类型的情况下正确的实例化对象(为了方便重复这个例子):



class Browser
{
public:
Browser();
Browser( const Browser&);
virtual Browser* construct()
{ return new Browser; } //虚默认构造器
virtual Browser* clone()
{ return new Browser(*this); } //虚拷贝构造器
virtual ~Browser();
//...
};
class HTMLEditor: public Browser
{
public:
HTMLEditor ();
HTMLEditor (const HTMLEditor &);
HTMLEditor * construct()
{ return new HTMLEditor; }//虚默认构造器
HTMLEditor * clone()
{ return new HTMLEditor (*this); } //虚拷贝构造器
virtual ~HTMLEditor();
//...
};

在有垃圾收集器的情况下,可以以下面的方式使用虚构造器:



void instantiate (Browser& br)
{
br.construct()->view();
}

再这里,系统自动将br.construct()返回指针注册为未命名指针,并且标注其为unreferenced指针,结果垃圾收集器可能稍后就销毁与之关联的对象收回它的存储空间。在没有垃圾收集器的情况下,instantiate()导致一个内存泄漏,因为分配的对象没有释放(这也可能导致未定义行为,因为分配的对象没有销毁)。为了使得编程习惯可用,垃圾收集器必须强制。你可能建议instantiate()写成这样:



void instantiate (Browser& br)
{
Browser *pbr = br.construct();
pbr->view();
delete pbr;
}

这样instantiate()也可以在没有垃圾收集器的情况下使用:当垃圾收集器打开时,delete语句被忽略(或许通过一些宏的技巧),而动态分配的对象在instantiate()退出之后自动释放。delete语句仅当垃圾收集器不活动时才执行。但是,又又其他狡猾的问题产生了。


后调用销毁器的问题

在没有垃圾收集器的环境中,在instantiate()退出时pbr正确的被释放,这意味着动态分配对象的销毁器也在这一点调用了。相反,垃圾收集器开启时,销毁器将在instantiate()退出之后的一个不确定的时间被销毁。程序员不能确定这到底在什么时候发生。到垃圾收集器的下一次工作可能只需要几秒,也可能需要几小时甚至几天。现在假定Browser的销毁器释放连接数据库的资源锁,锁或一个modem。在垃圾收集器开启时,程序的行为是不可预知的的——资源锁可能导致死锁,因为其他对象可能也在等待它。为了消除潜在的死锁,销毁器可能只执行一次不影响其他对象的操作,资源锁必须调用其他成员函数显式的释放。例如



void instantiate (Browser& br)
{
Browser *pbr = br.construct();
pbr->view();
pbr->release(); //释放所有的资源锁
delete pbr;
}

实际上,在有垃圾收集器的语言中这是主要技术。于是,为了保证在垃圾收集器环境和没有垃圾收集器环境的协同性,程序员不得不为类写一个专门的成员函数来释放资源锁——即使类是用于没有垃圾收集器的环境。这是不可接受的负担,也违反了"pay as you go"原则。从上面的讨论得出的结论是垃圾收集器不是可选的。写出适用于两种环境的高效和可靠的程序几乎是不可能的。要么使自动垃圾收集器成为程序完整的一部分,要么完全的剔除它(就像现在C++作的一样)。


时间临界应用程序

就像你看到的,垃圾收集器不能选择。为什么不能使它成为语言的一部分呢?实时系统建立在确定的时间计算之上。例如,必须在500微秒的时间片内执行完的函数决不能越过分配的时间片。但是,垃圾收集过程是非确定的——不可能预测它将在什么时候被调用,以及将要执行多久。因此,提供自动垃圾收集器的语言经常丧失在时间临界环境中使用的资格。注意实时程序不限于导弹发射和底层硬件处理;许多现代操作系统都包含控制在进程和线程之间分配资源的实时组件。许多通讯系统也是天然需要确定性的。为C++增加一个自动垃圾收集器将使C++丧失在这些应用领域应用的资格。因为关系密切的垃圾收集器也是不切实际的,目前从设计上来说C++就不是一种有垃圾收集器的语言。尽管加入垃圾收集机制是一件棘手的事,还是对增加垃圾收集器进行了严肃的讨论。决定是否加入,什么时间加入垃圾收集器太早了。


对象永久化


永久化对象可以存储在非易失存储器中,并在其他运行着的同一程序或不同程序中使用。永久化存储对象的存储叫serialization(序列化)。从永久存储器中重建serialized对象的过程叫deserializationreconstitution。其他面向对象语言直接通过库和内建关键字来支持对象永久化。C++并不直接支持对象永久化。设计一种高效、通用、平台无关的对象永久化模式失非常有挑战性的。本节示例了一种完成语言不支持的永久化的手工解决方法。手工对象永久化的困难和复杂化证明了语言内建支持的重要性。


具体对象的Serialization和Deserialization

Consider the following class:



class Date
{
private:
int day;
int month;
int year;
//构造器和销毁器
public:
Date(); //现在数据
~Date();
//...
};

存储Date对象是一个相当简单的操作:将每一个数据写入永久流(经常是一个本地磁盘文件,但是也可以是远程计算机的文件)。之后数据成员可以从存储器中读出。为了这个目的,另外需要另外两个成员函数,一个用于存储对象,另一个用于读出对象:



#include<fstream>
using namespace std;
class Date
{
//...
virtual ofstream& Write(ofstream&amp; archive);
virtual ifstream& Read(ifstream&amp; archive);
};
ofstream& Date::Write(ofstream& archive)
{
archive.write( reinterpret_cast<char*> (&day), sizeof(day));
archive.write( reinterpret_cast<char*> (&month), sizeof(month));
archive.write( reinterpret_cast<char*> (&month), sizeof(year));
return archive;
}
ifstream& Date::Read(ifstream& archive)
{
archive.read( reinterpret_cast<char*> (&day), sizeof(day));
archive.read( reinterpret_cast<char*> (&month), sizeof(month));
archive.read( reinterpret_cast<char*> (&month), sizeof(year));
return archive;
}

除了成员函数Read()Write(),还必须定义reconstituting 构造器,用来从流中读取serialized object:



Date::Date(ifstream& archive) //reconstituting 构造器
{
Read(arcive);
}

类层次体系

对于Date这样的成员都是基本类型的具体类,自己实现标准欠缺的永久化还是非常简单的。serialization和deserialization操作只不过是存储和读取数据成员。注意类的成员函数没有serialized。这是关心的主要问题,因为serialized 对象必须和对象在内存中的表示方法相同。


处理派生类和包含成员对象的的类要复杂的多:成员函数Read()Write()必须根据类层次中的每一个类重新定义。同样的对于每一个类都必须定义一个reconstituting 构造器,就像下面的例子这样:



class DateTime: public Date
{
private:
int secs;
int minutes;
int hours;
public:
//...
DateTime::DateTime(ifstream& archive); //reconstituting构造器
ofstream& Write(ofstream&amp; archive);
ifstream& Read(ifstream&amp; archive);
};
ofstream& DateTime::Write(ofstream&amp; archive)
{
Date::Write(archive); //must invoke base class Write() first
archive.write( reinterpret_cast<char*> (&), sizeof(day));
archive.write( reinterpret_cast<char*> (&month), sizeof(month));
archive.write( reinterpret_cast<char*> (&month), sizeof(year));
return archive;
}
ifstream& DateTime::Read(ifstream&amp; archive)
{
Date::Read(archive);
archive.read( reinterpret_cast<char*> (&day), sizeof(day));
archive.read( reinterpret_cast<char*> (&month), sizeof(month));
archive.read( reinterpret_cast<char*> (&month), sizeof(year));
return archive;
}
DateTime::DateTime(ifstream& archive) //reconstituting构造器
{
Read(arcive);
}

第三方类

覆盖成员函数Read()Write(),以及serializing数据成员到stream 或stream serializing 数据成员,有错误倾向而且维护困难。每当增加或移除数据成员时,或数据成员类型改变时,Read()Write()的实现就必须改变相关的成员函数——但者仍然时可管理的。但是,派生自没有定义reconstituting构造器和成员函数Read()Write()的类的派生类就更难处理了,因为派生类仅能serialize他自己的成员——不包括基类的成员。同样的问题存在于内部对象。子对象如何serialized?有些情况下克服这种困难也是可能的,虽然要花费很大的努力。例如,包含一个vector 的类可以通过迭代一个一个的serialize成员。尽管这是一半。一个vector的状态依赖其他参数,比如它的容量。如果vector对象自己不能被serialize,这些信息写到那里?Serializing数组是另一个含糊的地方。一个解决的方案是在每一个包含一定数量元素的serialized对象前面写一个头文件。但是,它不能和引用计数对象一起工作。大多数编译器的std::string都是引用计数的,这意味着在下面的代码片断中,5个string对象共享它们的数据成员:



#include <string>
using namespace std;
void single_string()
{
string sarr[4];
string s = sarr[0];
for (int i = 1; i< 4; i++)
{
sarr[i] = s;
}
}

引用计数是对用户隐藏的实现细节;不可能知道有多少string要表示和serialize一份数据。


手工对象永久化变得比它初看起来要复杂的多,不是吗?但是这还不是全部。如何将这种手工永久化模式用模板来表示?通过简单的象普通对象一样来存储模板特殊化,模式将不能表示存在于特殊化之间的关系。更糟的是,多继承和虚继承更具有挑战性。手工永久化模式如何保证虚子对象只被serialized一次,而不管继承树中存在多少对象?


标准永久化模式的提议

大多数程序员可能已经放弃了,这也很正常。解决虚基类的问题也是可能的,但一旦解决了这个问题,其他如函数对象,静态数据成员,引用变量以及共用体等更复杂的问题又会出现。手工永久化模式嗨有其他缺点:它不是标准的,同样的程序员必须自己实现它。结果是缺乏一致性,不同层次的可靠性和性能。没有标准支持的对象永久化,手工永久化模式是脆弱和错误倾向的。显然,没有标准的对象永久化,不可能保证简单,可移植性和高效的对象的serialization 和 deserialization。


基于库的扩展

一个标准化的永久化模式看起来象什么?有两种基本的策略。一个是基于库的,另一个依赖核心的语言扩展(关键字和语法)。基于库的解决方案在许多方面都是有利的。例如,它不需要扩展核心语言,如此就避免了为想要使用永久化对象的程序员增加新的负担。另外,一个库可以由编译器厂商换一种更好的实现方式,而不必更换不同的编译器。今天可以看到这种例子,程序员卸载就的由编译器厂商提供的就STL实现,再还一个其他的实现。尽管如此,基于库的解决方案还是由处理缺乏的语言的永久化的支持,而且它必须面对前面提到的困难和复杂性(使用最广泛的分布对象frameworks(就是DCOM和CORBA)的复杂性和奇特证明了这一点)。不靠内建的对模板和运算符重载的支持,STL决不可能变成今天这样。此外,语言的模板支持以多种方式扩展了STL需要的构造器(参见第二章“标准简报:ANSI/ISO C++的最新附加部分”和第九章)。同样的,对永久化的支持需要核心语言的扩展。


新的构造器类型

如果程序员没有显式的申明特殊成员函数,而且编译器需要他们,编译器会自动合成(参见第四章)。同样的,可以扩展一种新的构造器reconstituting constructor,在编译器需要的时候自动合成它,当然也可以由程序员申明。和其他构造器一样,允许程序员通过显式定义来覆盖默认的reconstituting 构造器。这种构造器的语法必须和其他构造器的形式不同。 特别的,reconstituting构造器应该和构造器的特征不同。换句话说,下面



class A
{
//...
public:
A(istream& repository ); //reconstituting ctor 或是普通构造器
};

是不推荐的。这样定义使得程序员的意图是要定义一个接受istream对象引用的普通构造器而不是一个reconstituting构造器。此外,这样的习惯可能会破坏现有的代码。更好的方法是增加一个专门表示reconstituting构造器的语法规则。例如,通过前缀符号><到构造器名字的前面



class A
{
//...
public:
><A(istream& repository ); //reconstituting constructor构造器
};

reconstituting 构造器可以接受一个流类型的参数。这个参数是可选的。当不给reconstituting 构造器实参,编译器从可以由编译器设置的默认输入流中deserializes对象(于默认的头文件位置一样)。为了使serialization 过程自动化,一个serializing销毁器也是需要的。这样的销毁器怎么申明?一个解决方法是增加其他类型的销毁器,这样类就有两种销毁器了。 但是这很麻烦,因为C++的对象模型是基于每个类一个销毁器的。增加一种销毁器就违反了这条规则。或许不需要定义不同的销毁器类型。作为代替,现有的销毁器可以自动完成serialization:编译器可以将执行必要的serialization操作的代码插入销毁器。(就像你知道的,编译器已经在用户定义的销毁器种调用基类和内部对象的销毁器。)


自动的serialization过程也有缺点。并不是所有类都需要serialized. serializing一个对象的开销仅仅应该在用户真真需要的时候再增加。此外,在serialization的时候遭遇运行期异常的可能性也是很高的。磁盘满,网络连接断,以及存储器的不可靠等等只是在将对象内容写入永久流中可能遇到的小部分运行期异常。但是,从销毁器中抛出异常是非常不受欢迎的(参见第六章),所以在对象销毁时自动serialization也是危险的。显然,不用显式调用成员函数来作这项工作是很难的。还有其他障碍:如何创建和serialization数组对象?如何与类定义的修改保持同步,如何在对象内容发生时保持serialized对象的同步?每一种支持对象永久化的语言都有自己的方法。C++也可以借用这些方法,或者创造一种革新的方法。


这些讨论给你一些关于为什么必须扩展语言以及他们将克服什么障碍的感觉。不管假象的讨论可能怎么样,C++的发展是一个民主的过程。许多修改和扩展都是由语言的用户而不是标准化委员会的成员发起的。STL大概是最好的例子。如果你有全面的扩展建议,你可以将它给标准化委员会。


对并行的支持


并行(Concurrency)是对多线程和多进程的通称。并行程序可以有效的增加性能和提高如字处理程序或人造卫星引导应用的响应速度。C++不直接处理多进程,线程和线程安全性。然而请注意,标准库中没有任何东西或语言自己的东西不允许并行。看下面异常处理的例子:早多线程环境下,异常处理必须是线程安全的,但是单线程环境可以以非线程安全的方法实现异常处理;这是一个依赖具体编译器的问题。允许编译器为并行提供必要的工具,而且实际上编译器也是这么做的。此外,没有程序语言的直接支持(既没有标准库也没有核心扩展),实现线程安全都是更复杂和移植性很差的。过去又许多建议为C和C++增加并行,但是现在还是没有语言对并行的直接支持。


多线程

多线程(Multithreading),与多进程不同,指在一个进程中使用多个控制的线程。多线程更容易设计和实现,而且它使应用程序可以更有效的使用系统资源。


应为在一个进程中的线程共享进程数据,所以同步的操作是很精简。为了这个目的,使用同步对象(synchronization objects)。不同类型的同步对象,比如互斥体、临界区、锁和信号量提供不同层次的资源分配和保护。不幸的是,同步对象的细节和特征因为平台不同而差异巨大。同步对象的标准库必须灵活到可以让用户可以联合平台特殊的同步对象和标准对象。这与在同一个程序中同时使用std::string和非标准string对象。作为选择,标准线程库可以提供同步对象的基本接口,而用平台相关的实现。但是引入标准对多线程支持时又有问题:单线程的OS比如DOS。尽管这些平台在今天不是很流行,但是他们仍然在使用,而且在这种系统上实现一个线程库几乎是不可能的。


线程安全

或许标准可以仅仅提供线程安全必要的特性而让其他特性——比如同步对象、时间对象、实例化、线程的销毁器等等——由编译器去实现,就像编译器厂商今天作的一样。线程安全保证对象可以安全的在多线程环境中使用。例如下面这个线程不安全的类



class Date
{
private:
int day;
int month;
int year;
public:
Date(); //current date
~Date();
//访问器
int getDay() const { return day; }
int getMonth() const { return month; }
int getYear() const { return year; }
//修改器
void setDay(int d) { day = d; }
void setMonth(int m) { month = m; }
void setYear(int y) { year = y; }
};

可以编程通过施用下面的修改成为线程安全的类:在每一个成员函数的开始,必须加一个锁;在每一个函数的返回点,锁必须被释放。


修改的函数看上去象下面这样:



void Date::setDay(int d)
{
get_lock();
day = d;
release_lock();
}
void Date::setMonth(int m)
{
get_lock();
month = m;
release_lock();
}
//etc.

这很乏味而且很容易自动完成。当前模式是非常适用"resource acquisition is initialization"习惯的时候(在第五章“面向对象的编程和设计”中讨论)。你可以定义类的构造器请求锁,由销毁器释放锁。例如



class LockDate
{
private:
Date& date;
public:
LockDate(const Date& d) : date { lock(&d); }
~LockDate() { release(&d); }
};

现实世界中的锁类可以模板化。它也提供超时设定和对异常的处理;但是对于讨论的LockDate的定义就足够了。Date的成员函数现在可以这样定义:



int Date::getDay() const
{
LockDate ld(this);
return day;
}
/...and so on
void Date::getDay(int d)
{
LockDate ld(this);
day = d;
}
//etc.

这看上去比原来的线程安全版本更好,但是它仍然是乏味的。但是标准C++仅仅走这儿。一个完全自动的线程安全需要核心语言的扩展。


从这个例子还看不出来为什么语言对线程安全的支持是必要的。毕竟,在每一个成员函数中实例化一个局域对象的复杂性和低效率并不是不可接受。继承的时候麻烦就来了。调用一个非线程安全的继承的成员函数可能导致未定义结果。为了保证继承的函数也又线程安全,Date的实现者必须覆盖每一个继承的成员函数。在每一个覆盖中加入锁。再调用基类的成员函数,最后再释放锁。再要语言小小的帮助,这个操作就可以变得更简单。


前方法和后方法(Before Method and After Method)


CLOS程序设计语言定义了概念前方法后方法。前方法是在方法之前预先执行的一系列操作。后方法是在方法执行成功之后的一系列操作。因此,CLOS中的每一个方法(成员函数)都可以认为是又构造器和销毁器的对象。CLOS为每一个用户定义方法提供默认前方法和后方法。默认情况下,前方法和后方法不作任何事情。但是用户可以覆盖他们来完成初始化和清除操作。只需要很小的修改就能在C++中采用这种概念,之后就能简单的实现线程安全类。一个趋势是为每一个类的成员函数提供一致的前方法和后方法。也就是说,前方法和后方法只定义一次,但是他们在类的每一个成员函数被调用的时候自动调用(除了构造器和销毁器)。这种方案的一个好处就是新增加到类的成员函数自动成为线程安全了,继承的成员函数也是一样。


可扩展成员函数


许多程序设计语言使用户可以在派生类中几乎自动的安排继承的成员函数。在C++中,派生类的成员函数覆盖而不是扩展基类的成员函数。通过在覆盖基类函数的派生类成员函数前面显式的调用基类的成员函数也可以达到扩展的效果(参见第五章)。下面的例子(再次在这里重复它)展示了改如何做:



class rectangle: public shape
{
//...
virtual void resize (int x, int y) //扩展基类的resize()
{
shape::resize(x, y); //显式的调用基类的虚成员函数
//增加功能
int size = x*y;
}
};

这里有两个问题。第一,如果基类名字改变,派生类的实现者必须查找每一个旧的名字再修改它们。


另一个问题是有许多成员函数就是用来扩展而不是覆盖的。最好的例子就是构造器和销毁器(幸运的是编译器自动扩展了他们),但是还是有其他例子。前面讨论的serialization和deserialization操作也需要再派生类中扩展而不是覆盖。


通过增加关键字super来解决第一个问题是很诱人的。Smalltalk和其他面向对象语言已经做了。为什么不让C++程序员享受它这项好处呢?super直接引用基类。可以象这样写代码:



class rectangle: public shape
{
//...
void resize (int x, int y) //扩展基类的resize()
{
super.resize(x, y); //基类的名字不再是必须的
//增加功能
int size = x*y;
}
};
class specialRect: public rectangle
{
void resize (int x, int y) //扩展基类的resize()
{
super.resize(x, y); //调用recatngle::resize()
//增加更多的功能
}
};

但是,在用了多继承的对象中的super是模棱两可的。可选的解决方案是增加一个不同的关键字到语言:extensible,来指示编译器自动在覆盖的成员函数前面加上对基类成员函数调用。例如



class shape
{
public:
extensible void resize();
}
class rectangle: public shape
{
public:
void resize (int x, int y) //扩展基类的resize()
{ //在这里隐含的调用了shape::resize()
//增加功能
int size = x*y;
}
};
class specialRect: public rectangle
{
void resize (int x, int y) //扩展基类的resize()
{ //隐含的调用recatngle::resize()
//...增加更多的功能
}
};

extensiblevirtual的一种特殊形式,所以后者可以省略。的确extensible解决了第一个问题:如果基类的名字改变,派生类的实现者不必改变成员函数的定义。第二个问题也解决了:在函数申明为extensible之后,编译器自动在基类中查找与派生类中对应的成员函数,在调用派生类成员函数之前先调用基类的成员函数。


动态链接库


典型的C++应用程序有包含所有代码和数据的静态连接可执行文件组成。尽管静态连接类型在速度上更有效率,但它是顽固的:每一次改变代码都需要完全重新编译。当时用动态链接库时,可执行文件不需要重新编译;下一次应用程序运行时,它自动载入新的库版本。动态链接库的优点是可以透明的更新动态链接库的新的发布版本。但是,如果在新版本库中对象的数据布局改变了,这种透明的"drop in"模式就会在C++对象模式下面失效;这是因为对象的大小和数据成员的偏移量是在编译期固定的。已经有建议扩展C++对象模式以使它更好的支持动态共享库。但是,代价是更慢的执行速度和大小。


基于规则的编程


许多商业数据库支持触发器(triggers)。触发器是用户定义的规则,其指示系统只要一个特定的数据值改变就自动的执行特定动作。例如,假定一个包含两个表Person和Bank Account的数据库。 在Bank Account中的每一行都与Person中的一个记录相关联。删除Person的一个记录自动触发删除所有与之关联的Bank Account中的记录。规则是在软件环境中触发器的等价物。AT&T贝尔实验室的William Tepfenhart和其他研究员已经扩展了C++以支持规则(UML and C++: A Practical Guide to Object-Oriented Development, p. 137)。扩展的语言叫R++(R表示"rules")。除了成员函数和数据成员,R++定义了第三种类成员:规则。规则由条件和关联的操作组成,操作在条件为真是自动执行。在C++中,为了决定关联的操作是否改执行还需要程序员手工的判断,通常通过switch语句或if语句。在R++中,这个测试是自动的——系统监视在规则列表中的数据成员,只要条件满足,规则就开始(就是,关联的操作执行)。基于规则的程序设计在人工智能、debugging系统和事件驱动系统中被广泛应用。为C++增加这个特性可以大大简化这些系统的设计和实现。


总结


语言的扩展要让操作的实现更容易,但扩展可能更困难甚至不可能的。然而这始终是一个折中的事情。使用一个类比,为汽车增加一个空调降低了它的燃料利用率,降低性能(Principles of Programming Languages: Design, Evaluation and Implementation, p. 327)。它是否是一个有益的折中依赖与各种因素,比如使用汽车地区的气候、燃料的价格、引擎的动力和用户自己的感受。注意空调总可以关掉以增加动力和增加燃料的使用效率。理想情况下,新的语言特性在不使用的时候将不增加任何性能损失。当程序员有意的使用他们时,他们应该尽可能的减小开销或更本不产生开销。但是,在空调和语言扩展之间有一个显著的区别:扩展和其他特性会相互作用。例如假象的关键字super就烦人的与多继承发生了矛盾。一个更现实的例子是模板的模板参数。在右边两个尖括号之间的空格是强制性的:



Vector <Vector<char*> > msg_que(10);

否则两个>>就会被解释成右移运算符。在其他情况下,互相作用更复杂:例如Koenig查找在有些情况下可以产生令人惊讶的结果(就像你在第八章“命名空间”读到的)。


本章展示了三种主要的扩展语言的建议:垃圾收集器、永久化和并行。不太激进的建议是扩展成员和规则。没有一个是可以轻松接受的。在标准化每一个建议中的棘手问题,在他们互相搅在一起之后更加复杂。例如一个永久化模式在线程安全环境中更加复杂。


考虑在过去的20年中C++设计者遇到的挑战,你可以保持乐观。如果你熟悉几个在标准之前就实现了容器类、RTTI、和异常处理的著名frameworks,你或许明白标准的容器类、RTTI和异常处理在所有方面都更好。如果本章讨论的特性成为C++标准一部分,也将是这种情况。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值