Date: Mon, 29 Jun 2020 08:49:41 -0500
On Fri, Jun 26, 2020, at 22:12, andrei--- via Std-Proposals wrote:
> 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.
After thinking about this for a few days I think this second statement is wrong: I think you want programing-by-contract with exceptions as one contract type. (post condition) By making exceptions a contract you unify with another part of the language instead of needing new syntax - this alone makes it more likely you can get someplace with your want. Contracts also give you more/better options for what if the exception happens anyway. Without going with contracts, checked exceptions are all or nothing which means almost nobody will retrofit them: they niche that only a few will use and thus not worth adding to the standard. With contracts you can start adding checked exceptions incrementally with choices for how to find out you are wrong, and choices for how to handle being wrong.
I don't follow contracts closely (though I feel like I should), but I think joining them and improving their papers to address exceptions is a better step for you.
> 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] <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
> --
> Std-Proposals mailing list
> Std-Proposals_at_[hidden]
> https://lists.isocpp.org/mailman/listinfo.cgi/std-proposals
>
> 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.
After thinking about this for a few days I think this second statement is wrong: I think you want programing-by-contract with exceptions as one contract type. (post condition) By making exceptions a contract you unify with another part of the language instead of needing new syntax - this alone makes it more likely you can get someplace with your want. Contracts also give you more/better options for what if the exception happens anyway. Without going with contracts, checked exceptions are all or nothing which means almost nobody will retrofit them: they niche that only a few will use and thus not worth adding to the standard. With contracts you can start adding checked exceptions incrementally with choices for how to find out you are wrong, and choices for how to handle being wrong.
I don't follow contracts closely (though I feel like I should), but I think joining them and improving their papers to address exceptions is a better step for you.
> 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] <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
> --
> Std-Proposals mailing list
> Std-Proposals_at_[hidden]
> https://lists.isocpp.org/mailman/listinfo.cgi/std-proposals
>
Received on 2020-06-29 08:53:15