On 16/10/2019 14:54, Öö Tiib wrote:
> On Wednesday, 16 October 2019 11:17:13 UTC+3, David Brown wrote:
>> On 15/10/2019 23:59, Soviet_Mario wrote:
>>> On 15/10/2019 13:57, David Brown wrote:
>>
>> Readability is of course very important - and code correctness even more
>> so. However, I am not convinced that exceptions are a great correctness
>> feature. It is hard to write fully exception-safe code. And, worse, it
>> is easy to write code that /appears/ to be exception safe, but is not.
>>
>> The two key problems, as I see it, are first that exceptions means all
>> sorts of code (function calls and expressions) can fail unexpectedly at
>> any time, leading to early returns from your function. You have to be
>> thinking all the time about what will happen if there is an exception
>> when the code runs. This can mean making more RAII classes, or more
>> indirect references (via unique_ptr, for example), simply to ensure
>> cleanup in case functions throw an exception. That will make the code
>> safe and correct - but can be of significant readability cost.
>
> Basically to live with exceptions all classes have to be RAII as bare
> minimum. Yes, unique_ptr is one of the handy components for to
> build (or on simpler case to typedef) such class but I don't
> understand what readability cost there is. Can you bring example
> what you meant?
>
I can try and give an outline of an example. Suppose you have code like
this, using a bool "success" return instead of exceptions:
bool success = tryThis();
doThisAnyway();
if (!success) return false;
continueProcessing();
return true;
How do you make that into an exception-based handling, with "tryThis"
throwing an exception on failure?
You can use a try block and re-throw:
try {
tryThis();
} catch (...) {
doThisAnyway();
throw();
}
doThisAnyway();
continueProcessing();
You've duplicated code as well as reducing the clarity.
Or you can make a class:
struct DoThisAnyway_later {
DoThisAnyway_later() {};
~DoThisAnyway_later() { doThisAnyway(); };
}
{
DoThisAnyway_later doThisAnywayLater();
tryThis();
// doThisAnyway called by destructor
}
continueProcessing();
Now you have re-arranged the order of the code, losing the correlation
between the textual order and the run-time order. (A general "scope
guard" template would reduce the code here, but not change the re-ordering.)
With explicit, manual control of the errors or unusual circumstances, it
can be easier to see the flow of the code.
And note that if you want to be sure that "doThisAnyway()" is done, and
"tryThis()" does not have a "noexcept" specifier, you have to have
something like these exception-safe codings just in case, because it
/might/ throw something. With a return-based solution, you know if it
is giving success feedback because it is in the return type (perhaps
something more sophisticated than a simple bool, such as a
std::optional, or a variant, or an "expected") - and you can mark it
with [[nodiscard]] to make sure the code checks the result.
I am not saying that exceptions /always/ make code harder to read - far
from it. I am merely saying that /sometimes/ they do.
(To be clear here, my kind of C++ programming is usually done with
exception support disabled in the compiler - partly for worst-case
performance reasons, partly for clarity, partly due to limited support
for some targets, partly due to a mix of C and C++ code. This limits my
experience of C++ exceptions.)
>> The other problem is that a lot of the information about C++ exceptions
>> that a function can throw, is basically lying. You can /say/ that a
>> function only throws specific exception types, which would be very
>> useful, but it's a lie. At least C++11 "noexcept" is a stronger, and
>> now "throw" specifications are no longer part of C++17 at all. But
>> there is currently no way to say what exceptions a function really could
>> throw, no way to check such features at compile time, no integration
>> with the type system, and the default is for functions to be specified
>> as "this function could throw anything, or pass on any exception" rather
>> than the more sensible "this function won't throw - it will do what it
>> says it will do".
>
> The exception specifications are often a lie anyway ... or hackable around.
> What we see in lot of bad code of languages where basically everything
> may throw? Something like that (Java or C# code):
>
> public void foo() throws Exception {/* ... */} // WTF?
>
> So there have to be tools and policies in place for to use the exceptions
> regardless of language. However it is not in any way better with checking
> error codes of function "success" returns. Latter just adds lot of
> required boilerplate code. Isn't that major readability cost?
Somewhere there is /always/ going to be a cost!
I think a lot of the readability cost of using function return values
for errors comes from the days of older C and older C compilers, where
you often had a mess of goto's in order to deal with errors underway
without having deeply nested "if" statements. Often you can get a lot
clearer code by simply dividing the code into several smaller functions,
with a style of "if (!success) return false;" as an exit. Before C99
"inline", and before compilers inlined single-use static functions
automatically, multiple small functions could quickly be costly in
efficiency.
And of course with C++ you have all the power of RAII, and tools like
std::unique_ptr - you don't need exceptions to use them. They apply
equally well to exiting functions with an early return.
>
>> (Static exceptions will, hopefully, improve on this a lot.)
>>
>>> I mean, often I feel the need of "consistent" (= UNIFORM) error
>> management.
>>
>> Consistency is good - but not if it is artificial consistency just for
>> consistency's sake.
>
> Every good project has to establish consistent project-wide error
> handling policy. It is likely possible to make it company-
> wide when company is working on closely related products
> in narrow problem domain. However I can not imagine
> programming-language-wide (or even industry-wide) error
> handling policies. May be with some different programming
> language (like Rust).
>
Certainly there is no one "ideal for every case" solution here.
>>
>>>
>>> Even if surely more performant, mixed management can be difficult to
>>> understand and use.
>>> So some importance maybe should also be given to the choice of using the
>>> same tool systematically for every problem of the same nature (errors),
>>> regardeless of its performance potential problems.
>>> Also "snippet" reuse is hindered by mixed use of both exceptions and
>>> return types, and maybe it's more error prone.
>>>
>>> One thing I really can't stand is the way C used to share a single
>>> variable (errno) to store status, that may be overwritten unpredictably
>>>
>>
>> Yes, errno is a very questionable idea. It made sense when it was
>> introduced, but leads to many limitations. It does allow some useful
>> practices, however, such as doing a string of calculations and then
>> checking for errno at the end rather than checking after each calculation.
>>
>> (It's not a single global any more - there is one errno per thread.)
>
> The errno is awful ... OTOH it seems to be the only semi-portable way
> in C++ to figure out what went wrong with lot of things (like with that
> std::basic_fstream::open() and the like) when it matters.
>
I am not an errno fan myself. But I can understand why it was made, and
that it could be useful for some kinds of coding.