Exception safety
Standard library functions often invoke operations that a user supplies
as function or template arguments. Some of these operations will occasionally
throw exceptions. Other functions, like allocator functions can also throw
exceptions. We must program in exception safe way to survive.
// Exceptions and the evaluation order:
void f( int expr1, int expr2);
int g( int expr);
int h( int expr);
f( g( expr1), h(expr2) ) // what is the evaluation order?
In the real word:
template <class T>
void f( T1*, T2* );
f( new T1, new T2); // what is the evaluation order of constructors?
The process of new expression:
1. It allocates memory
2. Constructs the object at that memory
3. If construction fails, memory will be freed
4. Otherwise returns with pointer to the memory
f( new T1, new T2); // what is the evaluation order of constructors?
A possible scenario:
1. Allocates memory for T1
2. Constructs the object T1
3. Allocates memory for T2
4. Constructs the object T2
5. Call f()
If 3. or 4. fails destructor of T1 won't be executed -> memory leak
Another possible scenario:
1. Allocates memory for T1
2. Allocates memory for T2
3. Constructs the object T1
4. Constructs the object T2
5. Call f()
If 3. fails then T1 is deallocated, but T2 is not -> memory leak
If 4. fails then T2 is deallocated, but T1 is not -> memory leak
f( auto_ptr<T1>, auto_ptr<T2>);
f( auto_ptr<T1>(new T1), auto_ptr<T2>(new T2));
A possible scenario:
1. Allocates memory for T1
2. Constructs the object T1
3. Allocates memory for T2
4. Constructs the object T2
5. Constructs auto_ptr<T1>
6. Constructs auto_ptr<T2>
7. Call f()
Same problems...
Solution:
f( auto_ptr<T1>, auto_ptr<T2>);
auto_ptr<T1> t1(new T1);
auto_ptr<T2> t2(new T2);
f( t1, t2);
//==============================================
Strong exception safety:
// dangerous version:
template <class T>
matrix<T> matrix<T>::operator=( const matrix &other)
{
if ( this != &other )
{
delete [] v;
copy( other);
}
return *this;
}
template <class T>
void matrix<T>::copy( const matrix &other)
{
x = other.x;
y = other.y;
v = new T[x*y];
for ( int i = 0; i < x*y; ++i )
v[i] = other.v[i];
}
// exception safe version:
template <class T>
matrix<T> matrix<T>::operator=( const matrix &other)
{
if ( this != &other )
{
matrix temp(other);
T *oldv = v;
// swap() in STL
x = temp.x;
y = temp.y;
v = temp.v;
temp.v = oldv;
}
return *this;
}
//==============================================
Moral:
#1 : Constructor function try block good only for translating exception
#2 : Destructor function try block is good for almost nothing
#3 : Other functions try block are equivalent with try block inside
the function body.
#4 : Do unmanaged resource allocation in constructor body,
never in the initializer list (or more better: use resource objects)
#5 : Clean up unmanaged resources in local try blocks
#6 : Exception specification of a constructor must be the union
#7 : Use Pimpl idiom if a base object can throw but you want survive
#8 : Prefer "resource allocation is initialization"
#9 : Perform every explicite resource allocation in its own statement
//===============================================
Exception safety in the standard library
void f(vector<X>& v, cosnt X& g)
{
v[2] = g; // X::operator= might throw exception
v.push_back(g); // vector<X> 's allocator might throw exception
sort( v.begin(), v.end() ); // X's less than might throw exception
vector<X> u = v; // X's copy constructor might throw exception
}
Theoretically there is two possibility:
- No guarantees: the container possibly corrupted
- Strong guarantees: all object will keep the invariant
In practice in the standard library:
- Basic guarantee for all operations:
basic invariants are kept, no memory leak or other resource problem
- Strong guarantee for key operations:
the operation succeeds or has no effect. This guarantee is provided
for some key operations, like push_back(), insert() on a list, etc...
- Nothrow guarantees for some operation:
some operations are guaranteed not to throw exception, like pop_back() or swap().
Basic and Strong guarantee suppose, that the user operations do not leave
the container operations in invalid state.
Guarantees for vector and deque:
- push_back(), push_front(): strong guarantee.
- insert(): strong, unless copy constructor or assignment of user class throws
- erase(): not throws, unless copy constr. or assignment of user class does it
- pop_back(), pop_front(): nothrows
Guarantees for list:
- insert(), push_back(), push_front(): strong guarantee.
- erase(), pop_back(), pop_front(), splice(), reverse(): nothrows
- remove(), remove_if(), unique(), sort(), merge(): not throws, unless
the user class predicate or comparison function does it.
Guarantees for associative containers:
- insert(): strong guarantee.
- erase(): nothrows.
Table:
vector deque list map
clear() nothrow(copy) nothrow(copy) nothrow nothrow
erase() nothrow(copy) nothrow(copy) nothrow nothrow
1-elem insert() strong(copy) strong(copy) strong strong
N-elem insert() strong(copy) strong(copy) strong basic
merge() -- -- nothrow(comp) --
push_back() strong strong strong --
push_front() -- strong strong --
pop_back() nothrow nothrow nothrow --
pop_front() -- nothrow nothrow --
remove() -- -- nothrow(comp) --
remove_if() -- -- nothrow(pred) --
reverse() -- -- nothrow --
splice() -- -- nothrow --
swap() nothrow nothrow nothrow nothrow
(copy-of-comp)
unique() -- -- nothrow(comp) --
std::string: basic_string<Ch, char_traits<Ch>, A>
- The char_traits from std does not throw exceptions: basic guarantee
- erase(), insert(), push_back() and swap(): strong guara