C++ Logo

std-proposals

Advanced search

Re: Re-purposing the function exception specification

From: <andrei_at_[hidden]>
Date: Sat, 27 Jun 2020 04:12:07 +0100
Good morning,

You have, indeed, convinced me that the "int foo() throws(auto)" is a
no-go and if it will never make it into the C++ language, the better for
that; your few well-chosen examples have been informative.

However, I'm still adamand that C++ needs to allow both un-checked and
checked exceptions for its practitioners, depending on their needs:

  * If I'm a writer of a shared library heavy on templates, then I'll
    never use this weird new feature, and my code will always be a valid
    C++.
  * If I'm a writer on a project where safety means all, I want checked
    exceptions even more than I want programming-by-contract.

It is true that you branded my 1st proposal for
checked-exceptions-at-templates too c++-98-ish - it was deliberately
named a "traditional" solution. One that lived (from C++98 to C++17)
longer, than a "decltype(auto)" did so far.

On the other hand - if the "exceptions checked at compile time" become
the part of C++, what's the possible fallout?

  * Most of C++ programmers will keep writing the same code they did
    before. They never write their functions to "throw(X)", they never
    care if the functions they call "throw(Y)", so nothing changes there.
  * A poor soul that wants to write a "database library" can now say
    "void read_next_record(void * recptr) throws (DbException)" and be
    sure the "DbException" is handled. If I remember correctly, the
    whole poing of exceptions was, many years ago, that "in some cases
    an error could not be ignored".
  * In a real project, it's very infrequent that new templates are
    written (templates are for the standard library). Instead, hundreds
    of specific classes will be defined, in multiple subsystems, with
    each subsystem defining its own "important" exceptions. If we can't
    enforce these "important" exceptions in the code, we're left to
    bleat about them in comments and documentation, both of which may or
    may not be read.
  * Enforced exception safety is better if we want it. C++ shall provide
    both.

On 22/06/2020 22:34, Arthur O'Dwyer wrote:
> On Mon, Jun 22, 2020 at 3:44 PM andrei_at_[hidden]
> <mailto:andrei_at_[hidden]> <andrei_at_[hidden]
> <mailto:andrei_at_[hidden]>> wrote:
>
> Thanks again for pointing to a dark corner in my proposal.
>
> For the template problem I can see at least 2 solutions (and would
> propose allowing both in the language).
>
> 1. Traditional-style: if the templates be the problem, let the
> templates be the solution:
> ============================================================
> class storage_exception { ... };
> class database_exception { ... };
>
> template <class T> struct storage_traits
> {
> const int max_record_length = 1000;
> using service_exception = storage_exception;
> };
>
> template <> struct storage_traits<database_storage>
> {
> const int max_record_length = 2000;
> using service_exception = database_exception;
> };
>
> class file_storage
> {
> bool read_record(char record[1000]) throw(storage_exception);
> };
>
> class database_storage
> {
> bool read_record(char record[2000]) throw(database_exception);
> };
>
> template <class T>
> void validate(T & storage) throw(typename
> storage_traits<T>::service_exception)
> {
> char record[storage_traits<T>::max_record_length];
> while (storage.read_record(record)) ...
> }
> ============================================================
>
>
> This strikes me as a correct solution, but it smells very C++98ish.
> This is basically analogous to C++98's
> template<class T>
> typename iterator_traits<T>::reference whatever(T it) {
> return *it;
> }
>
> It was very hard to use that sort of thing. We ended up with two
> different solutions to that problem:
> (1) Full-blown type inference.
> template<class T>
> decltype(auto) whatever(T it) { return *it; }
> This is analogous to your second solution below (but I don't like your
> second solution; see below).
> (2) decltype. That is, we give the programmer a way to say "This
> expression? Give me its type." (Similarly we have a way to say "This
> expression? Give me its noexceptness.")
> template<class T>
> decltype(*declval<T&>()) whatever(T it) { return *it; }
>
> Traits classes don't scale well for generic code because they always
> force bondage and discipline on /somebody/ — either the
> generic-algorithm-writer or the concrete-model-writer. Either the
> concept author says "`it+1` must always yield a value of exactly the
> type `T`" and then the concrete-model-writer has to deal with that
> constraint, or else the concept author says "`it+1` can yield anything
> lol" and then the generic-algorithm-writer has to deal with the
> constraint of never being able to mention `it+1` or else having to
> repeatedly do things in terms of `typename
> iterator_traits<T>::self_plus_1_type`. It's just awkward either way.
>
> The history of C++ suggests that we should invent a way to define the
> primary template of storage_traits as
> template<class T>
> struct storage_traits {
> using ...storage_exceptions = EXCEPTIONS_THROWN_BY(
> declval<T&>().read_record(nullptr) );
> };
> so that the user doesn't need to specialize it at all anymore. (I'm
> ignoring `storage_traits::max_record_length` because I think it's a
> distraction from the general case; in the general case we can't assume
> that a traits class will have any other motivation to exist.)
>
> So I agree that your first solution is simple and "in the spirit of
> C++", but I don't think it is practical, and I think users will not
> like it.
>
> 2. New-style: let the compiler deduce (the "auto" keyword already
> means "let compiler determine the type"!)
> ============================================================
> class file_storage
> {
> bool read_record(char record[1000]) throw(storage_exception);
> }
>
> class database_storage
> {
> bool read_record(char record[2000]) throw(database_exception);
> }
>
> template <class T>
> void validate(T & storage) throw(auto) // the compiler "knows"
> which "checked" exceptions are not handled
> {
> while (storage.read_record(record)) ... // the compiler "knows"
> what is "checked" for "T::readRecord()"
> }
> ============================================================
>
> `throw(auto)` smells like `noexcept(auto)` to me, which the Committee
> has repeatedly rejected. It syntactically appears in the signature
> (interface) of the function, but its actual meaning depends on the
> specific implementation of the function body.
> "Isn't that true of C++14 `auto` return types as well? And isn't it
> true of `constexpr` as well?" Yes, certainly. The Committee isn't
> consistent in its positions. :P But `auto` return types are awesome
> and super useful, so maybe they get more leeway. Even `noexcept(auto)`
> doesn't rise to the same level of usefulness as return-type-`auto`.
>
> Also, the user might well ask: "Okay, throw(auto) is awesome... but
> can't throw(auto) just be the default everywhere? Why do I have to
> explicitly /say/ that unhandled exceptions escape? That should be
> obvious!" Which of course defeats your whole point. But will the user
> care? They just want to write simple code. :)
>
> So, I think your second solution is reasonably usable (modulo the
> preceding paragraph), but I cannot foresee the Committee pursuing it
> at all. `throw(auto)` looks like `noexcept(auto)` and is Dead On
> Arrival for the same reasons.
>
> –Arthur

Received on 2020-06-26 22:15:26