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.
    • Not fatal, or expected errors.

    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 a function void addTwo()using different approaches to error handling.
    The function should read 2 lines, convert them to intand 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::stringreadLine();
    // Преобразовывает строку в int // В случае ошибки выбрасывает std::invalid_argumentintparseInt(conststd::string& str);
    // Складывает a и b// в случае переполнения выбрасывает std::overflow_error intsafeAdd(int a, int b);
    voidaddTwo(){
        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(conststd::exeption& e) {
            std::cout << e.what() << std::endl;
        }
    }

    Exceptions in C ++ allow you to handle errors centrally without too much лапши в коде,
    but you have to pay for it with a whole heap of problems.


    • the overhead of handling exceptions is quite large; you cannot often throw exceptions.
    • it is better not to throw exceptions from constructors / destructors and observe RAII.
    • By the signature of the function, it is impossible to understand what kind of exception can be removed from the function
    • the size of the binary file increases due to the additional code of support for exceptions.

    2. Return codes


    The classical approach inherited from C.


    boolreadLine(std::string& str);
    boolparseInt(conststd::string& str, int& result);
    boolsafeAdd(int a, int b, int& result);
    voidprocessError();
    voidaddTwo(){
        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]] booldoStuff();
    /* ... */
    doStuff(); // Предупреждение компилятора!bool ok = doStuff(); // Ок.

    You nodiscardcan also specify for a class, structure or enum class.
    In this case, the effect of the attribute is extended to all functions that return 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 appeared std::optional<T>.
    Let's see how the code looks now.


    std::optional<std::string> readLine();
    std::optional<int> parseInt(conststd::string& str);
    std::optional<int> safeAdd(int a, int b);
    voidaddTwo(){
        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.
    Can be replaced std::optionalby std::variant<ResultType, ValueType>.
    The code is obtained in the sense of the same as with std::optional, but more cumbersome.


    C ++ 2a and std :: expected


    std::expected<ResultType, ErrorType>- a special template type , it may fall into the nearest incomplete standard.
    It has 2 parameters.


    • ReusltType - expected value.
    • ErrorType- type of error.
      std::expectedmay 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::expectedwill be a monad .
    It is proposed to maintain a stack of operations on std::expectedboth of the Monad: map, catch_error, bind, unwrap, returnand then.
    With the use of these functions, it will be possible to associate function calls in a chain.


    getInt().map([](int i)return i * 2;)
            .map(integer_divide_by_2)
            .catch_error([](auto e)  return0; );

    Suppose we have functions with return std::expected.


    std::expected<std::string, std::runtime_error> readLine();
    std::expected<int, std::runtime_error> parseInt(conststd::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 (conststd::runtime_error& err) {
        std::cerr << err.what() << std::endl;
        return0;
    }

    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 the type of error, you can use existing standard types of exceptions std::runtime_error, std::out_of_rangeetc.


    If it is good to design the syntax, it std::expectedwill allow you to write 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 .

    Also popular now: