Document number: D2144R0
Project: Programming Language C++
Audience: LEWGI, LEWG
 
Antony Polukhin <antoshkka@gmail.com>, <antoshkka@yandex-team.ru>
 
Date: 2020-05-12

Back to "Throws: Nothing"

“Successful engineering is all about understanding how things break or fail”

― Henry Petroski

I. Introduction

During the discussion of the P1656 "Throws: Nothing" should be noexcept in Prague not all the important points were made. This paper brings additional information on use cases and P1656 breakages.

II. Points against "Throws: Nothing"

A. There's almost no functions without UB

For a moment let's consider this to be the part of function arguments. User kind-of provides it to the function, and in that case it may be a bad pointer. This bring UB to most of the member functions. For example std::vector::empty() now has an UB if called on bad pointer and should not be marked with noexcept.

Moreover, even pure functions without parameters may trigger UB as per stack overflow or signed integer overflow.

Treating noexcept as 100% guarantee that there's no UB is wrong. Current rules "noexcept means no UB if you do not do ugly things" worked well, but the borders of that rule are quite vague.

III. Points in favor of "Throws: Nothing"

A. User experience with Documentation

Due to more than 10 years of experience with noexcept users get used to the fact, that noexcept means "bullet proof safety" (committee use "wide contract" term to describe that).

Now take a look at the following function:

    constexpr basic_string_view(const CharT* s) noexcept;

The noexcept means "bullet proof safety" habit provokes users to not read the documentation for the function. noexcept increases the chances of misusing the function.

B. There are too many standard libraries for too many cases

Big companies tend to have their own standard libraries. Some of them are written from scratch, some of them are copy-pasted and patched opensource solutions.

There are many workloads where reacting on a contract violation by throwing an exception is perfectly valid and makes a lot of sense. Here's a real world example:

  1. Service under a high load with some caches. Calling std::terminate or std::abort looses the cache and in-process requests, forces the slow restart of the service and increased load on remaining instances. It is a perfectly valid desire to throw exceptions on contract violations rather than abort. In that case if someone updates of the database so that service starts calling optional::operator*() on an empty optional we do not terminate. With termination even a small rate of bad requests that dereference empty optional could yield all the instances irresponsible. That's much worse than a ~1% speed degradation on checks and throws.
  2. Software Watchdogs. If the monitored service responded with unexpected data it is much better to throw and catch an exception on contract violation, rather than terminate and do a slow restart. With exceptions we can force restart the bad service and keep reacting on signals from other services. With termination we loose time on watchdog restart, leaving the services uncontrolled for some time.

Forcing the noexcept policy for narrow contracts makes all those in-house standard libraries non conforming. C++ Standard does not satisfy the needs of those users any more.

C. The Contracts

We do not have Contracts as a language feature right now. Our understanding of Contracts is still vague, we have close to 0 in-field experience with them. But they are coming!

We've been living for 10 years with the "narrow contracts are not noexcept" hoping that Contracts could do a good job in that place.

With P1656 adoption we're killing one of the use-cases for Contracts, probably making them quite useless for the Standard library. Even if no standard library vendor plans to use contracts right now, there are in house standard libraries that may benefit from Contracts in "Throws: Nothing." functions.

D. noexcept everywhere is close to useless for the Standard Library

Standard library sometimes implements different algorithms depending on the results of std::is_nothrow_*_v for copy/move constructors, assignments and default constructors. In those cases we already have noexcept and it improves the codegen.

But it does not improve codegen for other cases.

Due to the Standard library being mostly a header only library modern compilers deduce noexcept specifiers by themselves. Moreover, standard C functions are nowadays mostly a built-in compiler intrinsics, so even for them noexcept makes almost no sense. Other places that could benefit from noexcept are already decorated by vendors (Standard permits that).

Additional noexcept in the Standard won't give you a better codegen.

E. std::terminate hell

This point is an outcome of bullet A. When users see noexcept in the docs they tend to propagate that noexcept to the enclosing function:

    basic_string_view::basic_string_view(const CharT* s, size_t s) noexcept;
    T& std::vector::back() noexcept;
    // ...

    std::vector<char> get_data_from_cache(std::string_view username) noexcept; 

    // vector::back() is noexcept, string_view constructor is noexcept => function is noexcept
    auto user_function1(std::vector<char>& v) noexcept {
        return std::string_view(&v.back(), 1);
    }

    auto user_function2(std::string_view username) noexcept {
        std::vector<char> v = get_data_from_cache(username); 
        return user_function1(v);
    }

But that does not play well with refactoring. Users put themselves in to the "std::terminate hell":

    basic_string_view::basic_string_view(const CharT* s, size_t s) noexcept;
    T& std::vector::back() noexcept;
    // ...

    std::vector<char> get_data_from_cache(std::string_view username) noexcept; 

    // Now we throw on empty vectors => function is NOT noexcept
    auto user_function1(std::vector<char>& v) {
        if (std::empty(v)) {
            throw std::out_of_range("user_function1 called with empty vector");
        }
        return std::string_view(&v.back(), 1);
    }

    // Other code relied on user_function1 being noexcept. Now it is broken.
    auto user_function2(std::string_view username) noexcept {
        std::vector<char> v = get_data_from_cache(username); 
        return user_function1(v); // sometimes terminates
    }

By making the functions with narrow contracts noexcept we provoke users to mark their own narrow contract functions with noexcept. That complicates the future refactoring and tends to be a bad practice.

IV. Conclusions

The noexcept specifier allows us to express a contract explicitly in the specification, but we should not use it to confuse users and provide no-throwing guarantees for Undefined Behavior cases.

We are designing not only interfaces, we are designing the Standard Library as a whole.

By forcing the noexcept instead of "Throws: Nothing" we:

V. Tentative polls

Postpone P1656 "Throws: Nothing" should be noexcept adoption and revise it after the Contracts land into the WD.

VI. Acknowledgements

TBD