[Herb Sutter 的名作More Exceptional C++中文版即将出版。作为本书译者,我很高兴将本书推荐给大家。征得华中科技大学出版社同意,我将公开部分译稿,敬请大家批评指正。]
泛型程序设计与C++标准库
C++威力强大的特性之一是对泛型程序设计(generic programming)的支持。这种威力直接反映在C++标准库的灵活性上,特别是它的容器、迭代器和算法部分,这一部分一直以来被称作标准模板库(STL)。
本书的开篇章节集中讨论如何最有效地使用C++标准库,尤其是STL。什么时候使用std::vector和std::deque会最有效?如何使用?在使用std::map和std::set的时候可能会碰到哪些陷阱?如何安全地避免这些陷阱?std::remove()为什么不能真正删除任何东西?
本章还特别介绍了一些有用的技巧和易犯的错误,在撰写自己的泛型程序代码的时候,包括撰写那些“用以和STL一起工作”或“用以扩充STL”的代码的时候,你会经常碰到它们。什么样的predicates才能安全地和STL一起使用?什么样的不行?为什么?要想让模板自身的行为可以改变,而且这种行为的改变是基于“与之协同工作的类型(type)”的能力,有什么现有的技术可以写出这种功能强大的泛型模板代码吗?如何在不同种类的输入输出流之间自如地切换?模板特殊化和重载是怎么一回事?“古怪”的typename关键字究竟有何过人之处?
随着对泛型程序设计和C++标准库有关话题的深入研究,我们还会碰到更多的问题。
条款1:流
难度:2
在动态地使用不同的输入输出流——包括标准控制台流(console stream)和文件时,最佳使用方式是什么?
1. std::cin和std::cout的类型是什么?
2. 写一个ECHO程序,让它简单地响应输入,并能通过以下两种方式等效地调用:
ECHO <infile> outfile
ECHO infile outfile
在大多数流行的命令行环境下,第一个命令假定程序从cin获得输入,并将输出发送到cout。第二个命令告诉程序从一个名为infile的文件中获得输入,并在名为outfile的文件中产生输出。这个程序应该能够支持以上所有的输入/输出选项。
解答
1. std::cin和std::cout的类型是什么?
简短地回答,cin实际上是:
std::basic_istream<char, std::char_traits<char> >
cout实际上是:
std::basic_ostream<char, std::char_traits<char> >
下面是较详细的回答,它通过一些标准的typedef和模板向你展示答案的来龙去脉。首先,cin和cout具有的类型分别是std::istream和std::ostream。接着,这些类型是std::basic_istream<char>和std::basic_ostream<char>的typedef。最后,考虑到模板参数的默认值,我们得到上面的答案。
注意:如果你使用的iostream子系统是C++标准制定之前的实现版本,你可能还会看到一些中间类(intermediate class),例如istream_with_assign。但这些类在标准中是不存在的。
2. 写一个ECHO程序,让它简单地响应输入,并能通过以下两种方式等效地调用:
ECHO <infile> outfile
ECHO infile outfile
最精简的方案
对于追求精简代码的人来说,最精简的方案莫过于下面这个程序,它仅包含一条语句:
// 例1-1:惊讶吗?只使用了一条语句
//
#include <fstream>
#include <iostream>
int main( int argc, char* argv[] )
{
using namespace std;
(argc > 2
? ofstream(argv[2], ios::out | ios::binary)
: cout)
<<
(argc > 1
? ifstream(argv[1], ios::in | ios::binary)
: cin)
.rdbuf();
}
这个方案之所以可行,得益于两个相辅相成的条件:第一,basic_ios提供了一个方便的rdbuf()成员函数,它返回某个流对象所使用的streambuf,在本例中,这个流对象也就是cin或临时ifstream对象,二者都派生于basic_ios。第二,basic_ostream提供了一个operator<<(),它正好接受这样的basic_streambuf对象,将其作为输入,然后将输入完全读取。正如法国人会说的那样,“C'est ca”(“就是这样!”)。
逐步趋向更灵活的方案
例1-1中的方案有两个主要缺点:首先,精简会带来晦涩,而且过度的精简不适合应用到产品代码中。
设计准则
尽量提高可读性。避免撰写精简代码(即,简洁但难以理解和维护)。避免晦涩。
第二,虽然例1-1回答了前面的问题,但只是在对输入进行逐字拷贝的情况下,这种方法才可行。这种功能在目前可能已经够用,但如果将来你需要对输入进行其它处理,例如将字符转换成大写,或是计算字符总数,或删除第三个字符,那该怎么办?这种需要在将来是很合理的;所以,最好我们现在就立即动手,将这些处理工作封装在一个单独的函数中,使这个函数可以多态地(polymorphically)使用正确类型的输入或输出对象:
#include <fstream>
#include <iostream>
int main( int argc, char* argv[] )
{
using namespace std;
fstream in, out;
if( argc > 1 ) in.open ( argv[1], ios::in | ios::binary );
if( argc > 2 ) out.open( argv[2], ios::out | ios::binary );
Process( in.is_open() ? in : cin,
out.is_open() ? out : cout );
}
但如何实现Process()?在C++中,主要有四种方法获得多态行为:虚函数、模板、重载和转换。其中,前两种方法可以直接用在这里,用以表达我们所需要的那种多态。
方法A:模板(编译时多态)
第一种方法使用的是编译时多态,这需要借助于模板;它只需要被传递的对象有一个合适的接口(例如一个名为rdbuf()的成员函数):
// 例1-2(a):模板化的Process()
//
template<typename In, typename Out>
void Process( In& in, Out& out ) {
// ... 执行某种更复杂的操作,
// 或只是简单的“out << in.rdbuf();”...
}
方法B:虚函数(运行时多态)
第二种方法使用的是运行时多态,它需要一个条件,即,存在一个具有合适接口的公共基类:
// 例1-2(b):第一次尝试,一定程度上可行
//
void Process( basic_istream<char>& in,
basic_ostream<char>& out )
{
// ... 执行某种更复杂的操作,
// 或只是简单的“out << in.rdbuf();”...
}
注意,在例1-2(b)中,Process()的参数类型不是basic_ios<char>&,因为那将不允许使用operator<<()。
毫无疑问,例1-2(b)中的方法具有依赖性,它要求输入和输出流必须分别从basic_istream<char>和basic_ostream<char>派生。这一点对我们的例子来说还不错,但要知道,并非所有的流都基于简单的char或者char_traits<char>。例如,宽字符流基于wchar_t,Exceptional C++ [Sutter00]的条款2和3也演示了一些具有不同行为特征的用户自定义traits(在那些例子中,ci_char_traits提供了大小写不分的行为特征),并展示了其潜在的用途。
因而,即使是采用方法B,我们也应该使用模板,让编译器去推导出合适的参数:
// 例1-2(c):更好的方案
//
template<typename C, typename T>
void Process( basic_istream<C,T>& in,
basic_ostream<C,T>& out )
{
// ... 执行某种更复杂的操作,
// 或只是简单的“out << in.rdbuf();”...
}
有效的工程设计原则
就其本身而言,以上所有答案都是“正确”的;但在目前场合下,我个人倾向于选择方法A。其原因归结于两条很有价值的设计准则。第一条是:
设计准则
尽量提高可扩充性。
避免写出的代码只能解决当前问题。几乎任何时候,若能写出可扩充的方案,那将是更佳选择——当然,只要我们不太过分。
均衡的判断力是有经验的程序员所具有的一个特征。尤其是,在“编写专用代码,只解决当前问题”(短视,难以扩充)和“编写一个宏大的通用框架去解决本来应该很简单的问题”(追求过度设计)之间,有经验的程序员懂得如何去获取最佳的平衡。
较之例1-1中的方案,方法A具有大致相同的整体复杂度,但除此之外,后者还更易于理解、更具可扩充性。较之方法B,方法A既简单又更具灵活性;它更能适应新的要求,因为它没有了束缚,不只是能和iostream体系打交道。
所以,如果存在两个选择,它们在设计和实现中需要的工作量相同,而且具有大致相当的清晰度和可维护性,那么,请尽量考虑可扩充性。这条建议并不是在教唆你,让你去对一个本来很简单的问题大动干戈——这方面大家以前已经做得够多了。相反,这条建议是一条鼓励:如果稍微思考一下就可以发现,自己正在解决的问题实际上是某个更通用的问题的特例,你就应该多做一些工作,而不要仅仅满足于解决当前问题。这条建议十分正确,因为,在设计中提高了可扩充性,往往意味着同时提高了封装性。
设计准则
尽量提高封装性。将关系分离。
只要有可能,一段代码——函数或类——应该只知道并且只负责一件事。
可以证明,方法A最出色的地方在于:它展示了关系之间有效的分离。它包括两部分代码,一部分代码知道输入/输出源(source)和目标(sink)中可能的区别,另一部分代码知道如何真正执行处理,这两部分代码被分离开来。这种分离也使得代码的用途更清晰,更易于他人阅读和理解。将关系进行有效的分离是好的工程设计的另一个特征,在本书条款中,我们将不断地看到这一点。