富有活力的语言需要不断改变和成长,C++也不例外。在本文中,Bjarne Stroustrup提出了自己对C++的设计和演化的看法。
为了让编译器、工具和类库实现者跟上节奏,让用户吸收标准C++所支持的编程技术,在早有预计的、沉寂了几年之后,委员会再次考虑语言扩展问题了。"扩展工作组"已经建立了,它代替了"演化工作组"。名称的改变(这是Tom Plum的建议)反映了更重要的是语言特性和标准类库工具的集成。我仍然是该工作组的主席。我希望这可以确保C++版本的连贯性和最终结果的一致性。相似的,委员会成员资格也显示了大量人员和组织的连续参与。幸运的是,也出现了很多新的面孔,为委员会带来了新的影响和新的专家意见。
我们打算对语言本身的改变保持谨慎和保守,重点强调兼容性。主要的目的是把主要的努力引导到标准类库的扩展上来。在标准类库方面,我们的目标是大胆进取,利用一切机会。
对于标准类库,我希望根据类库技术报告的要素来建立它,使它成为一个用于系统编程的更广泛的平台。例如,我希望看到用于某些领域的类库,例如目录/文件夹操作、线程和套接字。我还希望委员会同情很多新的C++程序员,提供类库工具支持背景不同的新手(不是新程序员和C的难民)。例如,我希望看到一个使用范围检查STL的标准方法。我对最频繁地被请求添加到标准类库中的标准GUI(图形用户接口)的期望值很低。但是,奇迹有时候也会发生--记得STL吗?
对于语言本身,我希望重点强调支持泛型编程的特性,因为泛型编程是语言的使用取得最大进步的领域。此处,我将调查两个关键部分:
·概念(Concepts):用于模板参数的类型系统
·初始化器(Initializer)列表:初始化工具的泛化
与以往一样,建议的数量仍然远远超出了委员会能够处理和该语言能够吸收的数量。请记住,接受所有好的建议是不可能办到的。
该语言扩展以支持泛型编程的全部目标是为工具提供更大的一致性,允许我们用泛型直接表示用于解决问题的类。
我的其它优先考虑(与更好地支持泛型编程一起)是更好地支持初学者。目前的建议有一种值得注意的倾向,即这些建议照顾了哪些提出和评估建议的专家用户。有些简单地帮助那些新手的建议经常被忽略了。我认为这是一种潜在的致命的设计偏好。除非新手受到了充分的支持,否则只有很少人能够成为专家。此外,很多人并不希望成为专家;他们希望仍然是"偶然的C++用户"。例如使用C++进行物理计算或控制试验设备的物理学家只有有限的学习编程技术的时间。计算机专家可能会在编程技术方面花费很多时间,而不仅仅是期望。我们必须消除那些采用优良技术的不必要的障碍。
一个非常简单的例子如下:
vector<vector<double>> v;
在98年的C++中,这会导致语法错误,因为>>是一个单独的词汇记号,而不是封闭模板参数列表的两个>。V正确的声明可能是:
vector< vector<double> > v;
我把它看作是一种阻碍。我曾经建议这个问题值得解决,但是当前的规则和演化工作组用一些很好的理由两次拒绝了我的建议。但是,这些理由都是语言技术方面的,而新手(包括其他语言的专家)没有兴趣。不接受第一种(也是十分)明显的v声明浪费了用户和教师的时间。我希望>>问题和其它相似的"阻碍"不要再出现在C++0x中。实际上,我与Francis Glassborow和其他人一起,正在试图系统地消除最频繁发生的这类"阻碍"。
另一个"阻碍"是:使用默认的复制操作(构造或赋值)来复制带有用户自定义析构函数的类对象是合法的。在这种情况下,要求用户自定义的复制操作将消除大量的、与资源管理相关的麻烦错误。例如,考虑下面这个过度简单化的字符串类:
class String {
public:
String(char* pp) :sz(strlen(pp)), p(new char[sz+1]) { strcpy(p,pp); }
~String() { delete[] p; }
char& operator[](int i) { return p[i]; }
private:
int sz;
char* p;
};
void f(char* x)
{
String s1(x);
String s2 = s1;
}
在构造s2之后,s1.p 和 s2.p指向相同的内存区域,而这块内存被删除了两次,可能导致灾难性的后果。这个问题对于经验丰富的C++程序员来说是很明显的,他们一般会提供适当的复制操作或禁止复制。但是,这个问题会严重地困扰新手,破坏其对语言的信任。
禁止带有指针成员的类对象的默认复制行为可能更好,但是这会导致令人厌烦的兼容性问题。修补长期存在的问题的难度比表面看起来要复杂很多,特别是在考虑C兼容性的时候。
1、概念(Concepts)
D&E(编者注:"C++的设计和演化"通常简称为D&E)关于模板的讨论中包含的关于模板参数的约束问题就占用了整整三页。很明显,我觉得应该需要一个更好的解决方案。在使用模板(例如标准类库的算法)的过程中出现的微小错误所导致的错误消息可能非常长,并且没有对我们没有任何帮助。这个问题是由于模板代码绝对相信自己的模板参数。看看下面的find_if():
template<class In, class Pred>
In find_if(In first, In last, Pred pred)
{
while (first!=last && !pred(*first)) ++first;
return first;
}
在上面的代码中,我们对In和Predicate类型作出了很多假设。从代码中我们可以看出,不知什么缘故,In必须用适当的语义支持!=、* 和++,并且我们必须能够把In对象复制为参数和返回值。类似的,我们可以看到,我们可以调用一个Pred,其参数是从In返回的任何类型的*(取值操作符),并给结果应用了!操作符,这个结果可以被当作是布尔型的。但是,在代码中所有的这些都是隐含的。标准类库仔细地记载转发迭代子(例子中的In)和谓词(Pred)的这些需求,但是编译器是不会阅读手册的。试试下面的错误,看你的编译器显示的错误信息:
find_if(1,5,3.14); // 错误
不完整的、但是十分高效的,以我的旧想法--让构造函数检查模板参数的假设条件--为基础的解决方案现在已经广泛使用了。例如:
template<class T> struct Forward_iterator {
static void constraints(T a) {
++a; a++; // 可以增加
T b = a; b = a; // 可以复制
*b = *a; // 可以废弃和复制结果
}
Forward_iterator() { void (*p)(T) = constraints; }
};
上面的代码定义了一个类,只有当T是一个转发迭代子的时候,它才能编译。但是,Forward_iterator对象没有做任何实际的事务,因此编译器只能(并且的确是)对这种对象做微乎其微的优化操作。我们可以在如下所示的定义中使用Forward_iterator:
template<class In, class Pred>
In find_if(In first, In last, Pred pred)
{
Forward_iterator<In>(); // 检查模板参数类型
while (first!=last && !pred(*first)) ++first;
return first;
}
Alex Stepanov和Jeremy Siek做了很多工作来开发和普及这种技术。他们使用这种技术的一个地方是Boost类库,但是目前你会在大多数标准类库实现中发现约束类。在错误消息的质量方面,它们的差异是很大的。
但是约束类最多是一个不完整的解决方案。例如,在定义中进行测试--如果检查工作只能在声明中完成,那么就会好很多。使用这种方式的时候,我们必须遵循接口的使用规则,并且可以开始考虑真正的模板分开编译的可能性问题。
因此,让我们告诉编译器我们所期望的模板参数:
template<Forward_iterator In, Predicate Pred>
In find_if(In first, In last, Pred pred);
假设我们能够表示出Forward_iterator和Predicate是什么,那么编译器现在可以不理会它的定义,单独地检查find_if()调用了。这时我们所需要做的工作是为模板参数建立一个类型系统。在现代C++环境中,这种"类型的类型(types of types)"被称为"概念(concepts)"。我们可以通过很多途径来说明这种概念;从现在开始,把它们想作是直接受到语言支持的、拥有更好的语法的约束类。一个概念说明了某种类型必须提供的什么工具,而不是说明它如何提供这些工具。完美的概念(例如<Forward_iterator In>)与数学抽象("对于所有的类型In,In可以被增加、销毁和复制")非常类似,如同最初的<class T>就是数学上的"对于所有的类型T"。
只要给出了find_if()的这种声明(并且不是定义)之后,我们就可以编写
int x = find_if(1,2,Less_than<int>(7));
这个调用会失败,因为int不支持*。换句话说,这个调用在编译时会失败,因为int不是一个Forward_iterator。重要的是,它使得编译器容易报告用户语言中的错误,并且在编译时,调用会被首先看到。
不幸的是,知道迭代子参数是Forward_iterator并且谓词参数是Predicate也不足以保证find_if()调用成功编译。这两个参数是互相影响的。特别是谓词的参数是一个使用*(pred(*first))解除引用的迭代子。我们的目的是在与调用分离的情况下,完善模板的检测,同时在不查看模板定义的情况下,完善每个调用的检查,因此概念必须有充分的表现能力,能够处理模板参数之中的这类迭代子。一种办法是用平行的参数来表示概念,这与模板的参数化方式类似。例如:
template<Value_type T,
Forward_iterator<T> In, // 迭代子在T序列中
Predicate<bool,T> Pred> // 带有 T 参数并返回一个布尔值
In find_if(In first, In last, Pred pred);
在上面的代码中,我们要求Forward_iterator必须指向类型T的元素,它也是Predicate的参数类型。
通过普通参数(此处是参数T)来表达模板参数之间必要的关系,很不幸没有强大的表现能力,导致我们添加模板参数,并且间接地(无法直接地)表达需求。例如,上面的例子不能说明把*first的结果作为参数传递给pred一定可行。其实,它说明的是Forward_iterator和Predicate共享了一个模板参数类型。为了处理这类问题,我们正在研究直接表达模板参数之间关系的可能性。例如:
template<Forward_iterator In, Predicate Pred>
where (assignable<In::value_type, Pred::argument_type>)
In find_if(In first, In last, Pred pred);
这种方法也有自己的问题,例如它的要求(where子句)趋向于增加模板定义本身的复杂性,并且流行的迭代子(例如int*)并不拥有成员类型(例如value_type)。
概念的一种可能的表达方式是直接支持我们过去使用的约束类这种表达方式。例如,我们采用如下的方式来定义前面例子中使用的Forward_iterator:
template <class T> concept Forward_iterator {
// 参数化的概念
Forward_iterator a;
++a; a++; // 可以增加
Forward_iterator b = a; b = a; // 可以复制
*b = *a; // 可以废除和复制结果
T x = *a; *a = x; // 可以认为结果是T类型的
};
或者
concept Forward_iterator { // 概念没有用参数表示
Forward_iterator a;
++a; a++; //可以增加
Forward_iterator b = a; b = a; //可以复制
*b = *a; // 可以废除和复制结果
};
参数化的概念定义可用于find_if的第一种声明,不带参数的用于第二种。它们表现了可替换使用的语言设计。我们在这个领域还会提供一些设计选择。但是,看看下面的情形:
int x = find_if(1,2,Less_than<int>(7));
这是不合格的,因为1和2是int型的,而int不支持*。如果我们使用参数化的概念设计,它也是不合格的,因为int不是一个能够与Forward_iterator<T>匹配的参数化类型。另一方面,看下面的例子:
void f(vector<int>& v, int* p, int n)
{
vector<int>::iterator q = find_if(v.begin(),v.end(),Less_than<int>(7));
int* q2 = find_if(p,p+n,Less_than<int>(7));
// …
}
很明显,我是在报告目前正在进行的工作,但是某种形式的概念成为C++0x的基石是很可能的。模板已经成为多数有效的(和高效的)C++编程样式的要素,但是它遭受很多困扰:大量的、无用的错误消息,缺乏基于模板参数重载模板的工具,分开编译很差。概念直接解决了所有这些问题,同时还没有基于方法的抽象基类的主要缺陷--通过虚拟函数调用的运行时解析的性能开销。重要的是,概念不依赖于显式声明的子类型层次,因此不需要逻辑冗余的层次关系,并且可以认为内建类型与类是平等的。
现在以概念和它与其它语言中相似的构造之间可能的关系为主题的论文很广泛。Matt Austern、Jaako J?rvi、Mich Marcus、Gabriel Dos Reis、Jeremy Siek、Alex Stepanov和我都活跃在这个设计问题的领域。
2、泛化的初始化器
C++的一个基本的想法是"对用户定义类型的支持如同内建类型一样好"。但是,看看下面的情形:
double vd[ ] = { 1.2, 2.3, 3.4, 4.5, 5.6 };
vector<double> v(vd, vd+5);
我们可以直接使用初始化器列表来初始化该数组,然而对vector来说,我们做得最好(指坏处最少)的方式就是建立一个数组并用该数组来初始化vector。如果只有少量几个初始化器值,我甚至于可能使用下面的方式来避免明确地说明初始化器值的数量(在上面的例子中是 5):
vector<double> v;
v.push_back(1.2);
v.push_back(2.3);
v.push_back(3.4);
v.push_back(4.5);
v.push_back(5.6);
我认为谁也无法适当地调用上面的任何解决方案。为了得到最容易维护的代码,并且不让内建(并且是天生危险的)数组受到的"宠爱"比推荐的用户定义类型多vector,我们可以编写下面的代码:
vector<double> v = { 1.2, 2.3, 3.4, 4.5, 5.6 };
或者
vector<double> v ({ 1.2, 2.3, 3.4, 4.5, 5.6 });
由于参数传递是在初始化过程中定义的,因此对于带有vector的函数来说,这也是可行的:
void f(const vector<double>& r);
// …
f({ 1.2, 2.3, 3.4, 4.5, 5.6 });
我相信这种初始化器的泛化会成为C++0x的一部分。它将成为构造函数检查工作的一部分,因为人们发现的很多缺陷都似乎可以通过构造函数的泛化(例如转发构造函数、有保障的编译期构造函数、继承的构造函数)来解决。