A couple of weeks ago, there was a main conference in the C ++ world - CPPCON .
Five days in a row from 8 am to 10 pm there were reports. Programmers of all denominations discussed the future of C ++, baited bikes and thought how to make C ++ easier.
Surprisingly, many reports were devoted to error handling. Well-established approaches do not allow to achieve maximum performance or may generate code sheets.
What innovations await us in C ++ 2a?
Conventionally, all erroneous situations in the program can be divided into 2 large groups:
After them, it makes no sense to continue execution.
For example, it is dereferencing a null pointer, driving through memory, dividing by 0, or violating other invariants in the code. All that needs to be done when they occur is to communicate as much information as possible about the problem and complete the program.
In C ++ too much there are already enough ways to complete the program:
Even libraries are starting to appear to collect data on crashes ( 1 , 2 , 3 ).
These are errors of appearance which are provided by the logic of the program. For example, errors when working with the network, converting an invalid string into a number, etc. The appearance of such errors in the program in the order of things. For their processing, there are several generally accepted tactics in C ++.
We will talk about them in more detail with a simple example:
Let's try to write the function void addTwo()
using different approaches to error handling.
The function should read 2 lines, convert them to int
and print the amount. It is necessary to process errors IO, overflow and conversion to a number. I will omit uninteresting implementation details. We will consider 3 main approaches.
// // IO std::runtime_error std::string readLine(); // int // std::invalid_argument int parseInt(const std::string& str); // a b // std::overflow_error int safeAdd(int a, int b); void addTwo() { try { std::string aStr = readLine(); std::string bStr = readLine(); int a = parseInt(aStr); int b = parseInt(bStr); std::cout << safeAdd(a, b) << std::endl; } catch(const std::exeption& e) { std::cout << e.what() << std::endl; } }
Exceptions in C ++ allow you to handle errors centrally without extra ,
but you have to pay for it with a whole bunch of problems.
The classical approach inherited from C.
bool readLine(std::string& str); bool parseInt(const std::string& str, int& result); bool safeAdd(int a, int b, int& result); void processError(); void addTwo() { std::string aStr; int ok = readLine(aStr); if (!ok) { processError(); return; } std::string bStr; ok = readLine(bStr); if (!ok) { processError(); return; } int a = 0; ok = parseInt(aStr, a); if (!ok) { processError(); return; } int b = 0; ok = parseInt(bStr, b); if (!ok) { processError(); return; } int result = 0; ok = safeAdd(a, b, result); if (!ok) { processError(); return; } std::cout << result << std::endl; }
Doesn't look so good?
In C ++ 17 nodiscard
.
If you specify it before declaring a function, the absence of a return value check will cause a compiler warning.
[[nodiscard]] bool doStuff(); /* ... */ doStuff(); // ! bool ok = doStuff(); // .
You can also specify a nodiscard
for a class, structure, or enum class.
In this case, the effect of the attribute will be extended to all functions returning values of the type marked nodiscard
.
enum class [[nodiscard]] ErrorCode { Exists, PermissionDenied }; ErrorCode createDir(); /* ... */ createDir();
I will not give the code with nodiscard
.
In C ++ 17, std::optional<T>
.
Let's see how the code looks now.
std::optional<std::string> readLine(); std::optional<int> parseInt(const std::string& str); std::optional<int> safeAdd(int a, int b); void addTwo() { std::optional<std::string> aStr = readLine(); std::optional<std::string> bStr = readLine(); if (aStr == std::nullopt || bStr == std::nullopt){ std::cerr << "Some input error" << std::endl; return; } std::optional<int> a = parseInt(*aStr); std::optional<int> b = parseInt(*bStr); if (!a || !b) { std::cerr << "Some parse error" << std::endl; return; } std::optional<int> result = safeAdd(*a, *b); if (!result) { std::cerr << "Integer overflow" << std::endl; return; } std::cout << *result << std::endl; }
You can remove in-out arguments from functions and the code will become cleaner.
However, we lose information about the error. It was not clear when and what went wrong.
You can replace std::optional
with std::variant<ResultType, ValueType>
.
The code will be the same as std::optional
, but more cumbersome.
std::expected<ResultType, ErrorType>
is a special template type , it may fall into the nearest unfinished standard.
It has 2 parameters.
ReusltType
- the expected value.ErrorType
- type of error.
std::expected
may contain either an expected value or an error. Working with this type will be something like this:
std::expected<int, string> ok = 0; expected<int, string> notOk = std::make_unexpected("something wrong");
How is this different from the usual variant
? What makes it special?
std::expected
will be a monad .
It is proposed to support a stack of operations on std::expected
as a monad: map
, catch_error
, bind
, unwrap
, return
and then
.
With the use of these functions, it will be possible to connect function calls in a chain.
getInt().map([](int i)return i * 2;) .map(integer_divide_by_2) .catch_error([](auto e) return 0; );
Let we have functions with return std::expected
.
std::expected<std::string, std::runtime_error> readLine(); std::expected<int, std::runtime_error> parseInt(const std::string& str); std::expected<int, std::runtime_error> safeAdd(int a, int b);
Below is only pseudo-code, it cannot be made to work in any modern compiler.
You can try to borrow from Haskell the do-syntax for recording operations on monads. Why not allow to do so:
std::expected<int, std::runtime_error> result = do { auto aStr <- readLine(); auto bStr <- readLine(); auto a <- parseInt(aStr); auto b <- parseInt(bStr); return safeAdd(a, b) }
Some authors suggest this syntax:
try { auto aStr = try readLine(); auto bStr = try readLine(); auto a = try parseInt(aStr); auto b = try parseInt(bStr); std::cout result << std::endl; return safeAdd(a, b) } catch (const std::runtime_error& err) { std::cerr << err.what() << std::endl; return 0; }
The compiler automatically converts such a block of code into a function call sequence. If at some point the function returns not what is expected of it, the chain of calculations will be interrupted. And as an error type, you can use the exception types that already exist in the standard: std::runtime_error
, std::out_of_range
, etc.
If it is good to design the syntax, then std::expected
will allow writing simple and efficient code.
There is no perfect way to handle errors. Until recently, C ++ had almost all possible ways to handle errors except monads.
In C ++ 2a, most likely ways will appear.
Source: https://habr.com/ru/post/426965/