I think I've found a solution to noexcept!
Hi!
I really like the N3248 paper because it discusses noexcept from a very
good perspective.
http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2011/n3248.pdf
There has been so much talk about the technical details around noexcept
but N3248 approaches noexcept from a user angle; it asks how noexcept
will be used in everyday programming in general and unit testing in
particular.
I have been thinking myself what the use case for noexcept would be.
Following is a suggestion on how to use noexcept but I haven't
investigated the technical details, so please halt me when I provide
something infeasible.
First of all I'm for statically checked noexcept (don't halt me yet) for
the plain reason that it should be a tool helping us in the design phase
of an application.
I tried to understand what problems noexcept will cause, i.e. what
started this argue about static/dynamic checking. In both camps we seem
to agree that everything with noexcept is fine until we get a failure.
So what failures do we get? I made a list with some that I could come up
with.
* Failure to meet preconditions, postconditions or invariants.
* Failure to allocate memory.
* Exhausting the stack.
* Segmentation fault.
* Division by zero.
* Failure to find an item in a list.
* File permission denied.
* Network down.
* etc.
Then I divided these problems into four categories.
* Expected failures (failure to find an item in a list, file permission
denied, network down, etc.)
* System failures (memory allocation, exhausting the stack)
* Usage failures (preconditions)
* Design failures (postconditions, invariants, segmentation fault,
division by zero)
If we look at these categories the /system failures/ can never be caught
in compile-time. Neither can /usage failures/ but with means of unit
tests we can prevent quite a lot of them from propagating into our code.
As for /design failures/, we can use the compiler to catch a lot of
errors, like for instance prohibit assigning NULL to a reference, using
const to prevent postcondition failures, and, according to Abrahams'
strong exception safety guarantee, using noexcept to prevent us from
operate on inconsistent objects. The latter can be achieved if we at
compile-time prohibit throwing of /expected failures/ in noexcept functions.
I think everyone agrees statically checking for design flaws is good,
the problem we face is that when we eventually get an error in run-time,
we have different opinions how to deal with them.
The big reason (correct me if I'm wrong) why some people want
dynamically checked noexcept is that so many functions may throw
std::bad_alloc which makes it difficult for us to use statically checked
noexcept. All of us would agree on having statically checked noexcept if
it wasn't for std::bad_alloc. Am I right?
Now let's go back and sort this out by means of our failure categories.
Here's my solution that I strongly believe will make both the static and
dynamic camps happy.
-------------------------------------------------------------
A function is ill-formed if declared with noexcept and calls a
non-noexcept function or throws an exception *except* for three
exception types; std::bad_alloc, std::runtime_error and std::logic_error.
In case std::bad_alloc, std::runtime_error, std::logic_error are thrown,
the handler std::atnoexcept will be called. The default behavior for
std::atnoexcept is to abort (if a new handler is set it will behave
similar to std::set_unexpected, see below). The stack is unwound for
custom handlers.
-------------------------------------------------------------
What have we achieved with this solution?
1. In our semantics contract we promise our users that a noexcept
function will never fail except in case of the three severe non-expected
circumstances:
- System failure "force majeure" when we get std::bad_alloc.
- Usage failure that should result in a std::runtime_error
- Design failure that should result in a std::logic_error.
2. We can still have the sought-after try/catch optimization (no
try/catch-block overhead) since it's the compiler, and not the run-time,
that checks for these severe exceptions.
3. The stack is unwound at an appropriate place so those using the
default std::atnoexcept handler with std::abort will have the stack
untouched for debugging, while others with customized handlers can go on
with a valid stack.
4. We can meet the mentioned unit test document N3248. See below.
Now, let's put this into effect and see how we can use this in practice.
First, for convenience, create three new custom made types of assertions:
// Usage failure assertion.
void precondition( bool expression ) noexcept
{
if( !expression )
throw std::runtime_error();
}
// Design failure assertion.
void invariant( bool expression ) noexcept
{
if( !expression )
throw std::logic_error();
}
// Design failure assertion.
class Postcondition
{
public:
Postcondition( bool expression ) noexcept
: expression_(expression)
{
}
~Postcondition() noexcept
{
if( !expression_ )
throw std::logic_error();
}
private:
bool expression_;
};
Then create our own std::atnoexcept handler to allow the three
exceptions std::bad_alloc, std::runtime_error and std::logic_error to pass.
static void myNothrowHandler() noexcept
{
// Just pass on the errors.
try
{
throw;
}
catch( std::bad_alloc& )
{
throw;
}
catch( std::runtime_error& )
{
throw;
}
catch( std::logic_error& )
{
throw;
}
// std::terminate automatically called if
// we reach here.
}
Now we can use this to create a first pilot case.
void mustNeverThrow( int position ) noexcept
{
precondition( position < 42 );
// Create a vector of size 42.
// May throw std::bad_alloc but that's legal.
std::vector<int> vec( 42 );
// Throws no exception.
vec[position] = -2;
// May throw std::out_of_range, but that's legal
// since it's derived from std::logic_error.
int i = vec.at( position );
invariant( i == -2 );
// For old functions we have to embrace them
// within try/catch(...)
int result;
try
{
result = oldSquareRoot( i );
}
catch(...)
{
// Something is wrong, oldSquareRoot should
// not throw.
throw std::logic_error();
}
invariant( result != -1 );
// We must embrace the throwing function
// connectToInternet() otherwise this
// mustNeverThrow() function will be ill-formed.
try
{
connectToInternet();
}
catch( NetworkDownException& )
{
// We can never prohibit people from swallowing
// exceptions, but at least this empty catch
// block should give a hint that the design is
// bad and that connectToInternet() maybe doesn't
// belong here.
}
}
int main()
{
std::atnoexcept( myNothrowHandler );
// Now we can handle the three failure types within
// our unit tests and/or application. And if we
// prefer an immediate termination or want to debug
// the code with a debugger we just remove or comment
// out our handler above.
try
{
mustNeverThrow( 15 );
}
catch( std::bad_alloc& )
{
std::cout << "System failure" <<
"Arrange more memory and try again";
}
catch( std::runtime_error& )
{
std::cout << "Usage failure" <<
"Email the users of this function";
}
catch( std::logic_error& )
{
std::cout << "Design failure" <<
"Email the programmer of this function";
}
return 0;
}
Do you think this is feasible?
Please comment.
Thanks,
Daniel
--
[ See http://www.gotw.ca/resources/clcm.htm for info about ]
[ comp.lang.c++.moderated. First time posters: Do this! ]