Document number: N2815=09-0005
2009-01-25
Rani Sharoni <rani_sharoni@hotmail.com>
Improving the standard library’s exception
specifications
Table of Contents
5 Default and move constructors
The current standard library’s exception specifications seems to be lacking in the sense that many operations that expected to have no-fail guarantee under certain conditions are not explicitly specified as such.
Having no-fail guarantee for certain operations is important to avoid error prone usability in which the operations are used in no-fail context (e.g. destructor) with the false assumption that they can’t throw. For example, unregister function using identifier key is usually a no-fail operation that might require lookup in associative container and therefore it can’t use std::map if map::find might fail.
Another concern is the ability to verify whether existing code is correct according to the standard when operation that might throw is actually not throwing in any existing implementation. Later on if the implementation will start throwing then it might silently break existing code.
Judging by the famous ‘The C++ programming language’ appendix E by Bjarne Stroustrup it appears that Stroustrup actually think that the standard already mandating some no-fail guarantees for certain operations and I mention that few times in this paper.
I’m using the term failure for errors that can be handled by the caller. This also includes exceptions for which no-fail is no-throw.
The following non-mutating operations should have unconditional no-fail
guarantee:
size, empty, max_size,
capacity, get_allocator, key_compare
and value_compare.
Iterators creation functions
should have unconditional no-fail guarantee:
begin, end, rbegin and
rend.
Non-mutating operations that depend on comparison function should have
unconditional no-fail guarantee unless the underlying comparison throws:
Container’s operators ‘==’, ‘<’, ‘!=’,
‘>’, ‘>=’ and ‘<=’.
Associative containers operations: find, count, lower_bound
and equal_range.
Proposed wording:
23.1.1 paragraph 10, add the following requirements:
- No size(), empty(), max_size(),
begin(), end(), cbegin(), cend(),
rbegin() or rend() function throws an exception.
- No container iterator’s member function throws an exception.
- No container iterator’s comparison operator throws an exception.
- No container’s ‘==’, ‘<’, ‘!=’, ‘>’, ‘>=’ or ‘<=’ operator throws an exception unless that exception is thrown by the contained object related comparison operator as defined by table 80.
- No get_allocator() function throw an exception unless that exception is thrown by the returned type copy or default constructor.
23.1.3 add to the end of paragraphs 13:
14 No at() throws if n < a.size().
23.1.3 add paragraphs 14:
14 No front(), back() or operator[] throws an
exception.
23.1.4.1 add paragraphs 4,5 and 6:
4 No find(), count(), lower_bound() or equal_range()
function throws an exception unless that exception is throws by the container’s
Pred object (if any).
5 No operator[] or at() function
throws an exception if the looked up element is present unless that exception is throws by the
container’s Pred object (if any).
6 No key_compare() or value_compare() function throws an exception unless that exception is thrown by the returned type copy or default constructor.
23.1.5.1 add paragraphs 5, 6 and 7:
5 No find(), count(), equal_range(), bucket_count(), max_bucket_count(),
bucket(), begin(n), end(n), cbegin(n), cend(n), bucket_size(), load_factor() and max_load_factor()
function throws an exception unless that exception is throws by the container’s
Hash or Pred object (if any).
6 No operator[] or at() function
throws an exception if the looked up element is present unless that exception is throws by the
container’s Hash or Pred object (if any).
7 No hash_function() or key_eq() function throws an exception unless that exception is thrown by the returned type copy or default constructor.
According to the standard the reason for having capacity (23.2.4.2) in addition
to size is in order to control the vector’s internal allocations. That’s mainly
useful for efficiency but also for having no-fail guarantee for insertion
operations of object’s with non-throwing default, copy constructor and
assignment operator.
The standard should explicitly say that vector insertion operations
(i.e. operator=, assign, resize, insert, push_back
and push_front) should not fail if there is
sufficient capacity and the contained object copying is not throwing.
shrink_to_fit is new vector/basic_string member for non-binding request to change the
capacity. shrink_to_fit
should not throw to make it easier to write compliant code using various
implementations.
Proposed wording:
23.2.6 add paragraphs 3 and 4:
3 No operator=(), assign(), resize(), insert(),
shrink_to_fit(), push_back()
or push_front() function throws an exception if there
is sufficient capacity unless the contained object copy, move constructor or
assignment operator throws.
4 No capacity() or
data() function throws an exception.
For vector and deque operator[X] is defined as
*(a.begin()
+ X) and due the iterators requirements proposal below
it can’t throw for valid access. Same applies for at().
operator[key] for associative containers is more problematic since
though it’s often used for lookup it’s actually an insertion operation for
which creation of pair with the key and default value is required (23.3.1.2).
Requiring no-fail in case that the object already exist make-sense but
it requires to change the definition of map’s operator[]
(better to avoid over-specifying definition via code).
Notice that existing implementations (e.g. Dinkumware
and libstdc++) already have the no-fail guarantee for lookup cases using lower_bound.
This also seems to be implied by the current N2800 draft.
Proposed wording added in this paper’s section 2.
basic_string is not an official container but it’s actually very similar to std::vector.
basic_string is more restrictive than vector in the sense that it can only contain POD types (21/1) and its traits are not throwing (21.1.1/1).
On the other hand, the current specifications of basic_string are intended to allow reference counted strings for which copy-on-write (COW) is essential. COW makes many of the traditionally no-fail operations to have might-throw behavior. For example, the non-const operator[] might throw for valid access in case that the reference count is more than one.
COW in operating systems doesn’t change the failure guarantee of specific operation since it will be practically impossible to program with that in mind. For example writing to static variable of integer type might trigger COW (e.g. the binary is shared between more than one processes) but still it’s a no-fail operation from the programmer’s point of view. Such transparency in respect to the usage (i.e. no additional constraints) is achieved using virtual memory and that’s doesn’t seem to be the case for basic_string COW in which optimization is significantly affecting the usability.
Most STL vendors give option to disable reference counted basic_string so the standard should explicitly relax the usability of non reference counted basic_string. Otherwise it’s practically impossible to write correct code in applications that requires low memory fault tolerance (e.g. sensitive to read-only usage of non-const string).
The relaxation for non reference counted basic_string should also be applied for the validity of references, pointers and iterators that refer to basic_string (e.g. swap is not invalidating).
It seems that for COW implementation cleanup operations like clear() and erase() can also fail.
Note: I noticed that the original paragraph 23.1/5, which mentioned reference counted implementations, had been removed so maybe COW is not allowed anymore per proposal N2668.
Proposed wording:
23.2.6 add paragraph 5 and 6:
5 Unless otherwise specified basic_string meet
the following additional requirements:
- No const member function throws an exception.
- No at() const member function throws an
exception if n < str.size().
- No const_iterator member function throws an exception.
- No const_iterator comparison operator throws an exception.
- No get_allocator() function throw an exception unless that exception is thrown by the returned type copy or default constructor.
6 Unless otherwise specified non copy-on-write basic_string
implementation meet the following additional requirements:
- No member function throws if there is sufficient capacity.
- No iterator member function throws an exception.
- No iterator comparison operator throws an exception.
- No at() member function throws an exception
if n < str.size().
All the standard container’s iterators operations should have no-fail guarantee. Non reference counted basic_string iterators should also not throw.
Bjarne Stroustrup seems to think that the standard actually already requires such non throwing behavior from iterators, in Appendix E.4.4 he writes:
Note that ++ and -- on an iterator can throw exceptions. […]
However, they cannot throw exceptions when moving an iterator from one element of a sequence to another, without violating the definition of ++ and -- on an iterator.
In Appendix E.7 Stroustrup also advise:
Don’t throw an exception from an iterator navigating a valid sequence.
Proposed wording added in this paper’s section 2.
All algorithms should not throw an exception unless the exception was originated from user defined operation.
Bjarne Stroustrup specifies this explicitly in Appendix E.5.3:
The algorithms themselves do not throw exceptions. Instead, they report errors and failures through their return values. […]
Thus, exceptions thrown from a standard algorithm must originate from a user-supplied operation. That is, the exception must come from an operation on an element – such as a predicate, an assignment, or a swap or from an allocator.
</quote>
Notice that std::list::sort specialized algorithm might throw in typical implementations in which the list’s default constructor might throw.
Proposed wording:
25 add paragraph 3:
3 No algorithm throws an exception unless that exception is throws by a user defined operation used by the algorithm.
I refer to constructor that accepts rvalue-reference of the same type as “move constructor” (e.g. X::X(X&&)).
Notice that default and move constructor of containers are tightly related due to the clear operations, for example consider the following:
string str1(“abc”);
string str2 = std::move(str1);
str1.clear(); // str1 is now like default constructed string
The above indicates that throwing default constructor usually implies throwing move constructor. It’s interesting to note that the current C++0x draft specifies that basic_string move constructor is not throwing.
The current standard allows to both default and move constructors of containers to throw an exception while it seems that in practice there is no good reason for throwing from such constructors of several containers.
Such no-fail guarantee relaxation will allow some useful use cases:
1) No fail transfer of ownership.
2) Safe cleanup outside of lock.
3) Creating wrapper that translate exceptions to error codes.
Some operations, like vector::push_back (23.2.6.4/1), require non-throwing move constructors (for the favor of strong exception safety) which mean that they can’t be used with standard containers (e.g. vector< list<int> >). Disallowing throwing move constructors might be too restrictive by itself since common way to implement move is by default construction (that might throw) + swap. It might be better to allow throwing move constructors with basic guarantee than to disallow it (silently) for legit use cases.
The following containers are typically implemented with no-fail guarantee for default constructor:
1) vector – capacity zero requires no allocation
2) basic_string – same as vector
3) list – head of the list can be a member
4) deque – same as list
5) map – same as list
6) multi_map – same as map
7) set – same as map
Non-throwing default constructor implies non-throwing move constructor since the later can be implemented using default construction and swap.
On the other hand, in might be unreasonable to force the implementation for having non-throwing default constructor for hash tables and that will be the same for the move constructor.
The Dinkumware implementation used by VC2008 might throw from
the default constructor of node based containers. I’ve been told that this is
essential for validity of the end() iterator after operations like swap. The same
implementation can also throw when compiled in “secure STL” mode for all
containers.
Proposed wording for containers (might be controversial):
23.1.1 paragraph 10 add sub-bullet:
- No container move constructor throws an exception unless that exception
is thrown by the allocator’s default or copy constructor.
- No container default constructor throws an exception unless that
exception is thrown by the allocator’s default or copy constructor.
Proposed wording for basic_string:
21.3.2 add to paragraph 1:
Throws: Nothing if the allocator's move constructor
throws nothing.
Acknowledgements
Thanks to Howard Hinnant and Dave Abrahams for
reviewing this paper.