It is strange that on Habré until now it was not mentioned about the clause to the C ++ standard called "Zero-overhead deterministic exceptions". I correct this annoying omission.
If you are worried about the overhead of exceptions, or you had to compile code without exception support, or just wondering what will happen with error handling in C ++ 2b (referring to a recent post ), please under the cat. You are waiting for squeeze out of everything that now can be found on the topic, and a couple of polls.
The conversation will continue to be conducted not only about static exceptions, but also about related proposals to the standard, and about any other ways of handling errors. If you came here to look at the syntax, then here it is:
double safe_divide(int x, int y) throws(arithmetic_error) { if (y == 0) { throw arithmetic_error::divide_by_zero; } else { return as_double(x) / y; } } void caller() noexcept { try { cout << safe_divide(5, 2); } catch (arithmetic_error e) { cout << e; } }
If the specific type of error is unimportant / unknown, then you can simply use throws
and catch (std::error e)
.
std::optional
and std::expected
Suppose we decided that an error that could potentially arise in a function is not “fatal” enough to throw an exception from it. Traditionally, error information is returned using the out parameter. For example, Filesystem TS offers a number of similar functions:
uintmax_t file_size(const path& p, error_code& ec);
(Do not throw an exception due to the fact that the file is not found?) However, the handling of error codes is cumbersome and prone to bugs. Error code is easy to forget to check. Modern code styles prohibit the use of output parameters; instead, it is recommended to return a structure containing the entire result.
Boost has been offering an elegant solution for some time to handle such “non-fatal” errors, which in certain scenarios can occur in the hundreds in the correct program:
expected<uintmax_t, error_code> file_size(const path& p);
The expected
type is similar to variant
, but provides a convenient interface for working with "result" and "error." By default, the “result” is stored in expected
. The implementation of file_size
might look something like this:
file_info* info = read_file_info(p); if (info != null) { uintmax_t size = info->size; return size; // <== } else { error_code error = get_error(); return std::unexpected(error); // <== }
If we are not interested in the cause of the error, or the error can only consist in the “absence” of the result, then you can use optional
:
optional<int> parse_int(const std::string& s); optional<U> get_or_null(map<T, U> m, const T& key);
In C ++ 17 from Boost, optional got into std (without support optional<T&>
); in C ++ 20, they will probably add the expected (this is only Proposal, thanks to RamzesXI for the amendment).
Contracts (not to be confused with concepts) - a new way to impose restrictions on the parameters of a function added in C ++ 20. Added 3 annotations:
double unsafe_at(vector<T> v, size_t i) [[expects: i < v.size()]]; double sqrt(double x) [[expects: x >= 0]] [[ensures ret: ret >= 0]]; value fetch_single(key e) { vector<value> result = fetch(vector<key>{e}); [[assert result.size() == 1]]; return v[0]; }
You can configure breach of contract:
std::terminate
It is impossible to continue the work of the program after a breach of contract, because compilers use guarantees from contracts to optimize the function code. If there is the slightest doubt that the contract will be fulfilled, it is worth adding an additional check.
The <system_error>
library added in C ++ 11 allows you to unify the handling of error codes in your program. std :: error_code consists of an error code of type int
and a pointer to an object of some class derived from std :: error_category . This object, in fact, plays the role of a table of virtual functions and determines the behavior of this std::error_code
.
To create your own std::error_code
, you must define your own std::error_category
and implement virtual methods, the most important of which is:
virtual std::string message(int c) const = 0;
You also need to create a global variable for your std::error_category
. Error handling with error_code + expected looks like this:
template <typename T> using result = expected<T, std::error_code>; my::file_handle open_internal(const std::string& name, int& error); auto open_file(const std::string& name) -> result<my::file> { int raw_error = 0; my::file_handle maybe_result = open_internal(name, &raw_error); std::error_code error{raw_error, my::filesystem_error}; if (error) { return unexpected{error}; } else { return my::file{maybe_result}; } }
It is important that in std::error_code
value of 0 means no error. If this is not the case for your error codes, then before converting the system error code to std::error_code
, you must replace code 0 with SUCCESS code, and vice versa.
All system error codes are described in errc and system_category . If at a certain stage the manual forwarding of error codes becomes too dreary, then you can always wrap the error code in the exception std::system_error
and throw it away.
Suppose you need to create another class of objects that own any resources. Most likely, you will want to make it uncopyable, but moveable, because it is inconvenient to work with unmoveable objects (they could not be returned from a function before C ++ 17).
But here's the problem: in any case, the moved object must be deleted. Therefore, a special "moved-from" state, that is, an "empty" object that does not remove anything, is necessary. It turns out that every C ++ class must have an empty state, that is, it is impossible to create a class with an invariant (guarantee) of correctness, from the constructor to the destructor. For example, it is impossible to create a correct class open_file
file, which is open throughout the lifetime. It is strange to observe this in one of the few languages that actively use RAII.
Another problem - dropping old objects when moving adds an overhead: filling std::vector<std::unique_ptr<T>>
can be up to 2 times slower than std::vector<T*>
because of the heap of old pointers when moving , with the subsequent removal of baby's dummies.
C ++ developers have long licked to Rust, where destructors are not called on displaced objects. This feature is called Destructive move. Unfortunately, Proposal Trivially relocatable does not offer to add it to C ++. But the problem of the overhead projector will solve.
A class is considered trivially relocatable if two operations: moving and deleting an old object are equivalent to memcpy from the old object to a new one. The old object is not deleted, the authors call it "drop it on the floor".
The type is Trivially relocatable from the compiler's point of view if one of the following (recursive) conditions is true:
int
or POD structure)[[trivially_relocatable]]
attribute [[trivially_relocatable]]
You can use this information using std::uninitialized_relocate
, which executes move init + delete in the usual way, or accelerated, if possible. It is proposed to mark as [[trivially_relocatable]]
most types of the standard library, including std::string
, std::vector
, std::unique_ptr
. The overhead std::vector<std::unique_ptr<T>>
with this Proposal disappears.
The C ++ exception mechanism was developed in 1992. Various implementation options have been proposed. As a result, the mechanism of exclusion tables was chosen, which guarantee the absence of an overhead projector for the main program execution path. Because from the very moment of their creation, it was assumed that exceptions should be thrown very rarely .
Disadvantages of dynamic (that is, ordinary) exceptions:
noexcept
. An exception can be thrown almost anywhere in the program, even where the author of the function does not expect it.Because of these shortcomings, the scope of exceptions is significantly limited. When exceptions cannot apply:
According to surveys, at work sites, 52% (!) Of developers are prohibited by corporate rules.
But exceptions are an integral part of C ++! Including the -fno-exceptions
flag, developers lose the ability to use much of the standard library. This further incites companies to impose their own "standard libraries" and yes, reinvent their own class strings.
But this is not the end. Exceptions are the only standard way to cancel the creation of an object in the constructor and give an error. When they are disabled, an abomination such as two-phase initialization appears. Operators cannot use error codes either, so they are replaced by functions like assign
.
Coat of arms Sutter (Herb Sutter) in P709 described a new mechanism for the transfer of exceptions. Ideally, the function returns std::expected
, but instead of a separate discriminator of the bool
type, which together with alignment will occupy up to 8 bytes on the stack, this bit of information is transferred in some faster way, for example, to Carry Flag.
Functions that do not touch CF (most of them) will be able to use static exceptions for free - both in the case of a normal return, and in the case of an exception pass! Functions that will be forced to save and restore it will receive a minimum overhead, and it will still be faster than std::expected
and any normal error codes.
Static exceptions look like this:
int safe_divide(int i, int j) throws(arithmetic_errc) { if (j == 0) throw arithmetic_errc::divide_by_zero; if (i == INT_MIN && j == -1) throw arithmetic_errc::integer_divide_overflows; return i / j; } double foo(double i, double j, double k) throws(arithmetic_errc) { return i + safe_divide(j, k); } double bar(int i, double j, double k) { try { cout << foo(i, j, k); } catch (erithmetic_errc e) { cout << e; } }
In the alternative version, it is proposed to require the try
keyword in the same expression as the call to the throws
function: try i + safe_divide(j, k)
. This will reduce the number of instances of using throws
functions in code, which is not safe for exceptions, to almost zero. In any case, unlike dynamic exceptions, the IDE will have the ability to somehow highlight expressions that throw exceptions.
The fact that the thrown exception is not stored separately, but is placed directly in place of the return value, imposes restrictions on the type of exception. First, it must be Trivially relocatable. Secondly, its size should not be very large (but it could be something like std::unique_ptr
), otherwise all functions will reserve more space on the stack.
The <system_error2>
library developed by Niall Douglas will contain status_code<T>
- “new, better” error_code
. The main differences from error_code
:
status_code
is a template type that can be used to store almost any conceivable error code (along with a pointer to status_code_category
), without using static exceptions.T
must be Trivially relocatable and replicable (the latter, IMHO, should not be mandatory). When copying and deleting, virtual functions are called from status_code_category
status_code
can store not only error data, but also additional information about a successfully completed operation.code.message()
does not return std::string
, but string_ref
is a rather heavy string type, which is a virtual “possibly owning” std::string_view
. There you can string_view
or string
, or std::shared_ptr<string>
, or some other crazy way to own a string. Niall claims that #include <string>
would make the <system_error2>
header impermissibly “heavy”Next, errored_status_code<T>
is entered - a wrapper over status_code<T>
with the following constructor:
errored_status_code(status_code<T>&& code) [[expects: code.failure() == true]] : code_(std::move(code)) {}
The default exception type ( throws
without a type), as well as the base exception type, to which all others are thrown (like std::exception
) is an error
. It is defined like this:
using error = errored_status_code<intptr_t>;
That is, an error
is such an “erroneous” status_code
for which the value is placed in 1 pointer. Since the status_code_category
mechanism provides correct deletion, movement and copying, theoretically, any data structure can be saved as an error
. In practice, this will be one of the following options:
std::exception_handle
, that is, a pointer to the thrown dynamic exceptionstatus_code_ptr
, that is, unique_ptr
to an arbitrary status_code<T>
.The problem is that case 3 does not plan to allow the error
to be brought back to status_code<T>
. The only thing you can do is get the message()
packed status_code<T>
. To be able to get back the value wrapped in error
, you need to throw it away as a dynamic exception (!), Then catch and wrap it in error
. In general, Niall believes that only error codes and string messages should be stored in error
, which is enough for any program.
To distinguish between different types of errors, it is proposed to use a “virtual” comparison operator:
try { open_file(name); } catch (std::error e) { if (e == filesystem_error::already_exists) { return; } else { throw my_exception("Unknown filesystem error, unable to continue"); } }
Using multiple catch blocks or dynamic_cast
to select the type of exception will not work!
A function can have one of the following specifications:
noexcept
: throws no exceptionsthrows(E)
: throws only static exceptionsthrows
implies noexcept
. If a dynamic exception is thrown from a “static” function, then it is wrapped in error
. If a static exception is thrown from a “dynamic” function, then it is wrapped in a status_error
exception. Example:
void foo() throws(arithmetic_errc) { throw erithmetic_errc::divide_by_zero; } void bar() throws { // arithmetic_errc intptr_t // error foo(); } void baz() { // error status_error bar(); } void qux() throws { // error status_error baz(); }
The proposal provides for the addition of exceptions to one of the future C standards, and these exceptions will be ABI-compatible with static C ++ exceptions. A structure similar to std::expected<T, U>
, the user will have to declare independently, although redundancy can be removed using macros. The syntax consists of (for simplicity, we will assume so) the keywords fails, failure, catch.
int invert(int x) fails(float) { if (x != 0) return 1 / x; else return failure(2.0f); } struct expected_int_float { union { int value; float error; }; _Bool failed; }; void caller() { expected_int_float result = catch(invert(5)); if (result.failed) { print_error(result.error); return; } print_success(result.value); }
In C ++, it will also be possible to call the fails
functions from C, declaring them in extern C
blocks. Thus, in C ++ there will be a whole constellation of keywords for dealing with exceptions:
throw()
- removed in C ++ 20noexcept
- the function specifier, the function does not throw dynamic exceptionsnoexcept(expression)
is a function specifier, the function does not throw dynamic exceptions providednoexcept(expression)
- does the expression throw dynamic exceptions?throws(E)
- function specifier, function throws static exceptionsthrows
= throws(std::error)
fails(E)
- a function imported from C throws static exceptions.So, in C ++, a cart of error-handling tools was delivered (more precisely, delivered). Then a logical question arises:
Errors are divided into several levels:
vector::at()
.std::optional
, std::expected
, std::variant
. Examples: stoi()
; vector::find()
; map::insert
.In the standard library, it will be most reliable to completely abandon the use of dynamic exceptions in order to make the compilation "without exceptions" legal.
Functions that use errno
to work quickly and minimally with the C and C ++ error codes should be replaced by fails(int)
and throws(std::errc)
, respectively. For some time the old and the new versions of the standard library functions will coexist, then the old ones will be declared obsolete.
Memory allocation errors are handled by the new_handler
global hook, which can:
Now std::bad_alloc
thrown by default. It is proposed to call std::terminate()
by default. If you need the old behavior, replace the handler with the one you need at the beginning of main()
.
All existing functions of the standard library will become noexcept
and will crash the program with std::bad_alloc
. At the same time, new APIs will be added, such as vector::try_push_back
, which allow memory allocation errors.
logic_error
Exceptions std::logic_error
, std::domain_error
, std::invalid_argument
, std::length_error
, std::out_of_range
, std::future_error
report a violation of the function precondition. In the new error model, contracts should be used instead. The listed exception types will not be declared obsolete, but almost all cases of their use in the standard library will be replaced by [[expects: …]]
.
Proposal is now in draft. He has already changed quite a lot, and he can still change a lot. Some developments did not have time to publish, so the proposed API <system_error2>
not entirely relevant.
The offer is described in 3 documents:
std::error
There is currently no compiler that supports static exceptions. Accordingly, it is not yet possible to make them benchmarks.
C++23. , , , C++26, , , .
, , . , . .
, ^^
Source: https://habr.com/ru/post/430690/
All Articles