【Exceptional C++(6)】异常处理的安全性
2018-01-26 本文已影响14人
downdemo
问题
- 实现如下异常-中立的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_;
unsigned vsize_; // v_区域大小
unsigned vused_; // v_区域中实际使用的T数目
};
说明
- 异常中立:代码引发异常时,异常能保持原样传递到外层调用代码
- 异常安全在Effective C++条款29提到过。当异常抛出时,有异常安全性的函数会不泄露任何资源,不允许数据破坏。异常安全函数必须提供三个保证之一才具有异常安全性
- 基本承诺:如果异常抛出,程序内任何事物仍保持有效状态,任何对象或数据结构都不被破坏,所有对象都满足前后一致
- 强烈保证:如果异常抛出,程序状态不改变,如果函数成功就是完全成功,否则回到调用函数之前的状态
- 不抛掷保证:承诺绝不抛出异常,它们总能完成它们原先承诺的功能,作用于内置类型上的操作都提供nothrow保证
解答
// 默认构造
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_; // 同上,这里也不能抛出异常
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一个元素
// 解决办法是把函数重构成void Stack<T>::Pop(T& result)
// 这样可以在栈状态改变前得知结果的拷贝是否成功
template<class T>
void Stack<T>::Pop(T& result)
{
if (vused_ > 0)
{
result = v_[vused_ - 1];
--vused_;
}
}
// 辅助函数
// 当把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;
}