On Mon, Jun 22, 2020 at 3:44 PM andrei@cybernetic.org <andrei@cybernetic.org> 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