Any operation that throws an exception has to bring the program back to a usable state. No resources may be leaked, and no objects may be left with broken internal invariants. The extent to which a function satisfies these criteria is called its exception safety.
Let us consider a simple class which holds two related values. This class, which respects the invariant that the first value is always less than or equal to the second, may be:
#include <iostream>
#include <stdexcept>
class Range
{
int low{};
int high{};
public:
Range(int l, int h) : low{l}, high{h}
{
if (low > high)
// Signal the error by throwing an exception
throw std::runtime_error{"invalid range"};
}
void set(int l, int h)
{
low = l;
if (l > h)
throw std::runtime_error{"invalid range"};
high = h;
}
void print() const
{
std::cout << low << ".." << high << '\n';
}
};
int main()
{
Range r{1, 5};
r.print();
return 0;
}
The constructor sets the invariant by verifying the two arguments before the object is created. The member function set tries to update both values. It first modifies the low value, then verifies the condition, then modifies the high value. When this call succeeds, the invariant holds and print will display the new bounds.
The same function can also deprecate the object.
#include <iostream>
#include <stdexcept>
class Range
{
int low{};
int high{};
public:
Range(int l, int h) : low{l}, high{h}
{
if (low > high)
throw std::runtime_error{"invalid range"};
}
void set(int l, int h)
{
low = l;
if (l > h)
throw std::runtime_error{"invalid range"};
high = h;
}
void print() const
{
std::cout << low << ".." << high << '\n';
}
};
int main()
{
Range r{1, 5};
try
{
// Call the setter to update the object state
r.set(10, 3);
}
catch (std::runtime_error e)
{
std::cout << e.what() << '\n';
}
r.print();
return 0;
}
The call r.set(10, 3) writes 10 to low and then throws. The assignment to high never happens, so the object now contains the values 10 and 5. The invariant is broken, but the object still exists.
At some later time, any use of r will operate on a corrupted range.
The sample function provides only basic exception guarantee; it did not leak resources, but it left the object modified and inconsistent.
A more robust assurance is achieved by doing all the work on temporary values, and only committing the changes if each step has succeeded.
#include <iostream>
#include <stdexcept>
class Range
{
int low{};
int high{};
public:
Range(int l, int h) : low{l}, high{h}
{
if (low > high)
throw std::runtime_error{"invalid range"};
}
void set(int l, int h)
{
if (l > h)
throw std::runtime_error{"invalid range"};
low = l;
high = h;
}
void print() const
{
std::cout << low << ".." << high << '\n';
}
};
int main()
{
Range r{1, 5};
try
{
r.set(10, 3);
}
// Handle the matching exception here
catch (std::runtime_error e)
{
std::cout << e.what() << '\n';
}
r.print();
return 0;
}
The check now happens prior to changing any member. If the new bounds are invalid, the throw will happen before the object is changed from its initial values. If the check succeeds, both values are assigned.
In the example, the call throws, so the catch statement prints the message, and print still displays the original range 1..5.
The object was never left in an intermediate invalid state. This is the strong exception guarantee: either the code runs to completion or the state of the program remains unchanged.
A more formidable assurance certain operations can give is the no throw guarantee, assuring that they will never throw. Assignments of built in types and destruction of no throw objects are such operations. Such a function may be declared noexcept so the compiler and the human eye know it won't throw.
Exception safety is thus a list of conditions that a function promises if an exception leaves its body. The basic guarantee states that no resource leaks will occur.
0 comments
No comments yet. Be the first.