【问题】
1.实现如下异常-中立(exception-neutral) container。要求:Stack对象的状态必须保持一致性;即使有内部操作抛出异常,Stack对象也必须可以析构;T的异常必须能够传递到调用者那里。
template <class T>
class Stack
{
public:
Stack();
~Stack();
Stack(const Stack&);
Stack& operator = (const Stack&);
unsigned Count(); //返回T在栈中里面的数目
void Push(const T&);
T Pop(); //如果为空,则返回缺省构造出来的T
private:
T* v_; //指向一个用于vsize_ T的对象足够大的内存空间
unsigned vsize_; //区域的大小
unsigned vused_; //区域中实际使用的T的数目
};
2.根据当前的C++标准,标准库中的container是异常-安全的(exception-safe)还是异常-中立(exception-neutral)的?
3.应该让容易container成为异常-中立的吗?为什么?有什么折中的方案?
4.Container应该使用异常规则吗?比如,我们到底应不应该使用“Stack::Stack() throw(bad_alloc);”的声明?
挑战极限问题:
5.由于目前许多的编译器中使用try和catch会给你的程序带来一些额外的负担,所以在我们这种低级的可复用(reusable)Container中,最好避免使用它们。你能在不使用try和catch的情况下,按照要求实现Stack所有的成员函数吗?
在这里提供两个例子以供参考:
template <class T>
Stack<T>::Stack()
:v_(0),vsize_(10),
vused_(0)
{
v_ = new T[vsize_]; //初始化的内存分配
}
template<class T>
T Stack<T>::Pop()
{
T result; //如果为空,则返回缺省构造出来的T
if(vused_ > 0)
{
result = v_[--vused_];
}
return result;
}
【解答】
现在来看看实现,我们对T有一个要求,就是T的析构函数(destructor)不能抛出异常。这是因为,如果允许T的析构函数抛出异常,那我们就很难甚至不可能在保证代码安全性的前提下进行实现了。
//default constructor
template <class T>
Stack<T>::Stack()
:v_(new T[10]), //缺省的内存分配(创建对象)
vsize_(10),
vused_(0)
{
//如果程序到达这里,说明构造过程没有问题。
}
//拷贝构造函数
template<class T>
Stack<T>::Stack(const Stack<T>& other)
:v_(0), //没分配内存,也没有被使用
vsize_(other.vsize_),
vused_(other.vused_)
{
v_ = NewCopy(other.v_,other.vsize_,other.vsize_);
//如果程序到达这里,说明拷贝构造过程没有问题
}
//拷贝赋值
template<class T>
Stack<T>& Stack<T>::operator = (const Stack<T>& other)
{
if(this != &other)
{
T* v_new = NewCopy(other.v_,other.vsize_,other.vsize_);
//如果程序到达这里,说明拷贝构造过程没有问题
delete []v_;
//这里不能抛出异常,因为T的析构函数不能抛异常:
//::operator delete []被声明为throw();
v_ = v_new;
vsize_ = other.vsize_;
vused_ = other.vused_;
}
return *this;
}
//析构函数
template<class T>
Stack<T>::~Stack()
{
delete[] _v; //同上,这里也不能抛异常
}
//计数
template<class T>
unsigned Stack<T>::Count()
{
return vused_; //内建类型,不会有问题
}
//push操作
template<class T>
void Stack<T>::Push(const T& t)
{
if(vused_ == vsize_) //可能随着需求增长
{
unsigned vsize_new = (vsize_+1)*2; //增长因子
T* v_new = NewCopy(v_,vsize_,vsize_new);
//如果程序到达这里,说明拷贝构造过程没有问题
delete []v_;
//这里不能抛出异常,因为T的析构函数不能抛异常:
//::operator delete []被声明为throw();
v_ = v_new;
vsize_ = vsize_new;
}
v_[vused_] = t; //如果这里抛异常,增加操作则不会执行
++vused_; //状态也不会改变
}
//pop操作
template<class T>
T Stack<T>::Pop()
{
T result;
if(vused_ > 0)
{
result = v_[vused_-1]; //如果这里抛异常,相减操作不会执行
--vused_; //状态也不会改变
}
return result;
}
对于上述代码,Pop()强迫使用者编写非异常-安全的代码,这首先就产生一个负面效应(即从栈中pop出一个元素);其次。这可能导致遗漏某些异常(比如将返回值拷贝到调用者的目标代码上)。
同时这也表明,很难编写异常-安全的代码一个原因就是因为它不仅影响代码的实现部分,而且还影响接口。某些接口,不可能完全保证异常-安全的情况下被实现。
解决这个问题的一个可行的方法是把函数重新构造成如下,这样我们就可以知道在栈的状态下之前得知结果是否拷贝成功了。举例子,这是一个更具有异常-安全的Pop():
//pop操作
template<class T>
void Stack<T>::Pop(T& result)
{
if(vused_ > 0)
{
result = v_[vused_-1]; //如果这里抛异常,相减操作不会执行
--vused_; //状态也不会改变
}
}
这里我们可以让Pop()返回void,然后再提供一个Front()成员函数用来访问顶端的对象。
下面是辅助函数的实现:当我们需要把T从缓存区拷贝到一个更大的缓冲区时,这个辅助函数会帮助分配新的缓冲区,并把元素原样拷贝过来,如果在这里发生异常,辅助函数会释放占用所得的临时资源,并把异常传递出去,保证不发生内存泄露。
template<class T>
T* NewCopy(const T* src,unsigned srcsize,unsigned destsize)
{
destsize = max(srcsize,destsize);
T* dest = new T[destsize];
//如果程序到达这里,说明分配和构造都没问题。
try
{
copy(src,src + srcsize,dest);
}
catch(...)
{
delete[] dest;
throw; //重新抛出异常
}
//如果程序到达这里,说明拷贝操作也没问题。
return dest;
}
对附加题的解答:
2.根据当前的C++标准,标准库中的container是异常-安全的(exception-safe)还是异常-中立(exception-neutral)的?
关于这个问题,目前还没明确的说法。委员会讨论对涉及应该提供并保证弱异常安全性(即容器总是可以进行析构操作),还是应该提供并保证强异常安全性(即所有的容器操作都要从语意上具有“要么执行,要么撤销(commit-or-rollback)的特性”)。如果实现了对弱异常安全性的保证,那么强异常安全性也很容易保证了。
3.应该让容易container成为异常-中立的吗?为什么?有什么折中的方案?
有时候,为了保证某些container成为异常-中立,其内的某些操作将会不可避免的付出一些空间代价。可见异常-中立本身并不错,但是当实现强异常安全性所付出的空间代或时间代价远远大于实现弱异常安全性。要实现异常-中立性就不太现实了。有一个折中的方法就是,用文档记录下T中不被允许抛出异常,然后通过遵循这些文档规则来保存其异常-中立性。
4.Container应该使用异常规则吗?比如,我们到底应不应该使用“Stack::Stack() throw(bad_alloc);”的声明?
答案是否定的。我们不能这么做,因为我们预先并不知道哪些操作会抛异常,也不知道抛什么样的异常。
挑战极限问题:
5.由于目前许多的编译器中使用try和catch会给你的程序带来一些额外的负担,所以在我们这种低级的可复用(reusable)Container中,最好避免使用它们。你能在不使用try和catch的情况下,按照要求实现Stack所有的成员函数吗?
是的,这是可行的,因为我们仅仅只需要捕获“...”部分,如下代码可被改写:
try{
TryCode();
}
catch(...)
{
CatchCode(parms);
throw;
}
//可被如下代码替换
struct Janitor{
Janitor(Parms p):pa(p){}
~Janitor()
{
if uncaught_exception() CatchCode(pa);
}
Parms pa;
};
{
Janitor j(parms); //j被析构,如果TryCode成功或抛异常
TryCode();
}
如下重写NewCopy函数:
template<class T>
T* NewCopy(const T* src,unsigned srcsize,unsigned destsize)
{
destsize = max(srcsize,destsize);
struct Janitor{
Janitor(T* p):pa(p){}
~Janitor(){if(uncaught_exception()) delete[] pa;}
T* pa;
};
T* dest = new T[destsize];
Janitor j(dest);
copy(src,src+srcsize,dest);
}
作者和几个擅长靠经验来进行速度测试的人讨论过上述问题。结论是在没有异常发生的情况下,try和catch往往比其他方法快得多,而且今后还可能更快。但尽管如此,这种避免使用try和catch的技术还是非常重要的,一来是因为有时候就是需要写一些比较规整、比较容易维护的代码;二来是因为现在有一些编译器在处理try和catch的时候,无论在产生或不产生异常的情况下,都会生成效率低下的代码。