更多 C++ 惯用法/泛型容器惯用法
创建泛型容器类(向量、列表、堆栈),对它们的值类型施加最小的要求。这些要求只是复制构造函数和非抛出析构函数。
如果需要真正的泛型容器(如 STL),那么在 C++ 中开发泛型容器可能会变得很复杂。放宽对类型 T 的要求是开发真正泛型容器的关键。有几个 C++ 惯用法实际上可以实现对类型 T 的“最低公分母”要求。
让我们以堆栈为例。
template<class T>
class Stack
{
int size_;
T * array_;
int top_;
public:
Stack (int size=10)
: size_(size),
array_ (new T [size]), // T must support default construction
top_(0)
{ }
void push (const T & value)
{
array_[top_++] = value; // T must support assignment operator.
}
T pop ()
{
return array_[--top_]; // T must support copy-construction. No destructor is called here
}
~Stack () throw() { delete [] array_; } // T must support non-throwing destructor
};
除了某些数组边界问题外,上面的实现看起来非常明显。但它很天真。它对类型 T 的要求比实际需要的更多。上面的实现要求在类型 T 上定义以下操作
- T 的默认构造函数
- T 的复制构造函数
- T 的非抛出析构函数
- T 的复制赋值运算符
理想情况下,堆栈不应该在其中构造比对其执行的 push 操作次数更多的对象。类似地,在每次 pop 操作之后,应该从堆栈中弹出并销毁一个对象。上面的实现没有做到这一点。其中一个原因是它使用了类型 T 的默认构造函数,这完全没有必要。
实际上,使用构造和销毁泛型容器惯用法,可以将对类型 T 的要求减少到以下内容。
- 复制构造函数
- 非抛出析构函数。
为了实现这一点,泛型容器应该能够分配未初始化的内存,并且只在“初始化”每个元素时调用一次构造函数。这可以通过以下三个泛型容器惯用法来实现
#include <algorithm>
// construct helper using placement new:
template <class T1, class T2>
void construct (T1 &p, const T2 &value)
{
new (&p) T1(value); // T must support copy-constructor
}
// destroy helper to invoke destructor explicitly.
template <class T>
void destroy (T const &t) throw ()
{
t.~T(); // T must support non-throwing destructor
}
template<class T>
class Stack
{
int size_;
T * array_;
int top_;
public:
Stack (int size=10)
: size_(size),
array_ (static_cast <T *>(::operator new (sizeof (T) * size))), // T need not support default construction
top_(0)
{ }
void push (const T & value)
{
construct (array_[top_++], value); // T need not support assignment operator.
}
T top ()
{
return array_[top_ - 1]; // T should support copy construction
}
void pop()
{
destroy (array_[--top_]); // T destroyed
}
~Stack () throw()
{
std::for_each(array_, array_ + top_, destroy<T>);
::operator delete(array_); // Global scope operator delete.
}
};
class X
{
public:
X (int) {} // No default constructor for X.
private:
X & operator = (const X &); // assignment operator is private
};
int main (void)
{
Stack <X> s; // X works with Stack!
return 0;
}
operator new 分配未初始化的内存。它是一种调用 malloc 的花哨方法。construct 辅助模板函数调用就地 new,进而对初始化的内存调用复制构造函数。指针 p 应该是在使用 operator new 分配的未初始化内存块中。如果 end 是一个指向容器最后一个初始化元素之后的元素的迭代器,那么从 end 到 end_of_allocation 范围内的指针不应指向类型 T 的对象,而应指向未初始化的内存。当从容器中删除元素时,应该调用它们的析构函数。如所示,destroy 辅助函数在这里可能会有所帮助。类似地,要删除一个范围,另一个重载的 destroy 函数可以接受两个迭代器。它本质上对序列中的每个元素调用第一个 destroy 辅助函数。
所有 STL 容器都采用类似的技术。它们对模板参数类型有尽可能少的限制。另一方面,一些流行的 C++ 库对可参数化类型的要求比必要的高。
还有其他几个泛型容器惯用法。
设计异常安全的泛型容器 -- Herb Sutter