⬆️ ⬇️

What will happen with error handling in C ++ 2a

image



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?



Some theory



Conventionally, all erroneous situations in the program can be divided into 2 large groups:





Fatal errors



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 ).



Not fatal errors



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.



1. Exceptions



 //     //   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.





2. Return codes



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?



  1. Cannot return the actual value of the function.
  2. It's very easy to forget to handle the error (when was the last time you checked the return code from printf?).
  3. You have to write error handling code next to each function. Such code is harder to read.

    Using C ++ 17 and C ++ 2a we will consistently fix all these problems.


3. C ++ 17 and nodiscard



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 .



C ++ 17 std :: optional



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.



C ++ 2a and std :: expected



std::expected<ResultType, ErrorType> is a special template type , it may fall into the nearest unfinished standard.

It has 2 parameters.





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.



Conclusion



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.



What to read and look at the topic



  1. Actual proposal .
  2. Speech about std :: expected c CPPCON .
  3. Andrei Alexandrescu about std :: expected on C ++ Russia .
  4. More or less fresh discussion of the proposal for Reddit .


')

Source: https://habr.com/ru/post/426965/



All Articles