另一个"阻碍"是:使用默认的复制操作(构造或赋值)来复制带有用户自定义析构函数的类对象是合法的。在这种情况下,要求用户自定义的复制操作将消除大量的、与资源管理相关的麻烦错误。例如,考虑下面这个过度简单化的字符串类:
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)的这些需求,但是编译器是不会阅读手册的。试试下面的错误,看你的编译器显示的错误信息:
不完整的、但是十分高效的,以我的旧想法--让构造函数检查模板参数的假设条件--为基础的解决方案现在已经广泛使用了。例如:
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的参数类型。