C++ Logo

std-proposals

Advanced search

Re: Re-purposing the function exception specification

From: Arthur O'Dwyer <arthur.j.odwyer_at_[hidden]>
Date: Mon, 22 Jun 2020 17:34:41 -0400
On Mon, Jun 22, 2020 at 3:44 PM andrei_at_[hidden] <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-22 16:38:04