[Spice-devel] [PATCH 12/22] Add exception handling classes
Christophe de Dinechin
cdupontd at redhat.com
Thu Mar 8 09:56:55 UTC 2018
> On 7 Mar 2018, at 12:52, Frediano Ziglio <fziglio at redhat.com> wrote:
>
>>> On 2 Mar 2018, at 18:13, Christophe Fergeau <cfergeau at redhat.com> wrote:
>>>
>>> On Fri, Mar 02, 2018 at 02:00:23PM +0100, Christophe de Dinechin wrote:
>>>>
>>>>
>>>>> On 2 Mar 2018, at 09:03, Frediano Ziglio <fziglio at redhat.com> wrote:
>>>>>
>>>>>>>
>>>>>>> On 1 Mar 2018, at 13:13, Frediano Ziglio <fziglio at redhat.com> wrote:
>>>>>>>
>>>>>>>>
>>>>>>>> From: Christophe de Dinechin <dinechin at redhat.com>
>>>>>>>>
>>>>>>>> Throwing 'runtime_error' directly should be reserved for the support
>>>>>>>> library. Add an 'Error' class as a base class for all errors thrown
>>>>>>>> by the streaming agent, as well as subclasses used to discriminate
>>>>>>>> between categories of error.
>>>>>>>>
>>>>>>>> The Error class offers:
>>>>>>>> - a 'format_message' member function that formats a message without
>>>>>>>> allocating memory, so that there is no risk of throwing another
>>>>>>>> exception at throw time.
>>>>>>>>
>>>>>>>
>>>>>>> why not formatting the message constructing the object so before the
>>>>>>> throw?
>>>>>>
>>>>>> That requires dynamic allocation, which is entirely unnecessary and
>>>>>> potentially dangerous (replacing one exception kind with another).
>>>>>>
>>>>>
>>>>> As explained by my string.c_str example your interface is prone to
>>>>> errors.
>>>>
>>>> The interface is not “prone to errors” at all. The scenario you
>>>> describe makes no sense, since the whole interface is designed to
>>>> avoid dynamic strings. This is now explicitlyi documented (see
>>>> https://github.com/c3d/spice-streaming-agent/blob/c%2B%2B-refactoring/include/spice-streaming-agent/errors.hpp,
>>>> not yet shared, I’m not entirely done with the reviews). The
>>>> constructor comment now says:
>>>>
>>>> Constructor takes the base message returned by what()
>>>> The message must remain valid over the whole lifetime of the exception.
>>>> It is recommended to only use static C strings, since formatting of any
>>>> dynamic argument is done by 'format_message'
>>>
>>> This behaviour seems inconsistent with std::runtime_error(const char *)
>>> which as far as I can tell does not have this lifetime expectation.
>>
>> You are correct, but please note that std::runtime_error takes a string as
>> input argument, making it very clear that you are dealing with a ‘string’
>> underneath, not a const char*.
>>
>>>
>>>>
>>>> Anyway, your example is misleading. Anybody that passes the result of
>>>> string::c_str() to a const char * without proper consideration for the
>>>> lifetime is at fault. The const char * interface itself cannot be
>>>> faulted for that.
>>>
>>> For what it's worth, I usually expect const char * interfaces to make
>>> their own copy of the const char * arg if they need it after they
>>> return, and I've rarely seen exceptions to this expectation.
>>
>> std::vector and all standard library containers? You do a push_back of a
>> const char * in an std::vector<string>, you get a copy. If you do that in an
>> std::vector<const char *>, you don’t.
>>
>> In C++, raw pointers, including const char *, always have the explicit
>> meaning that you manage the lifetime. So if you have “rarely seen
>> exceptions”, you did not look hard enough IMHO ;-)
>>
>> Let me give an example from *our own code*. ConcreteConfigureOption takes two
>> const char * arguments. Does it make copies? No. Same thing with the related
>> AddOption method. Anything particularly surprising there? I don’t think so.
>>
>> Of course, we could switch to “strings everywhere” (which Lukas suggested at
>> a point). But that’s wrong too, it causes unnecessary allocations and code
>> bloat (see below what I’m talking about).
>>
>>>
>>> If I understood properly, things are designed this way to avoid running out
>>> of memory while building the exception. I'm definitely not in favour of
>>> having an error-prone API to handle a very rare case which the rest of the
>>> code is most likely not going to be able to cope with anyway.
>>
>> Yet another iteration of the “bad_alloc” theory. You know that this is
>> starting to get on my nerves? As stated *REPEATEDLY* before, bad_alloc is
>> only one of the reasons for doing things that way.
>>
>> To make this clear, let me elaborate a little on *one* other reason (I listed
>> a few others), code bloat and runtime cost. Let’s say we want to store a
>> write error with an errno. The two approaches under consideration are:
>>
>> A. throw WriteError(message, operation, errno);
>> B. throw std::runtime_error(message + “ in “ + operation + “: “ +
>> strerror(errno))
>>
>> I’ll briefly mention, as a gentle reminder of some of the “other reasons”,
>> that (A) is
>> 1. shorter to type
>> 2. easier toi read
>> 3. less error prone
>> 4. more likely to guarantee consistent messages
>>
>> and I will instead focus on the code being generated in both cases.
>>
>> In case A, the code is roughly equivalent to:
>>
>> tmp = WriteError_constructor(message, operation, errno)
>> runtime_cxa_throw(tmp)
>>
>> So roughy two calls, passing a total of four register args, no exception
>> landing pad.
>>
>> In case B, the code is roughly equivalent to (where LPn is a “landing pad”, a
>> piece of code that cleans up if an exception is thrown):
>>
>> tmp1 = string_constructor(message); // LP1
>> tmp2 = string_constructor(“ in “); // LP2
>> tmp3 = string_operator_plus(tmp1, tmp2); // LP3
>> tmp4 = string_constructor(operation); // LP4
>> tmp5 = string_operator_plus(tmp3, tmp4); // LP5
>> tmp6 = strerror(errno);
>> tmp7 = string_constructor(tmp6); // LP6
>> tmp8 = string_operator_plus(tmp5, tmp7); // LP7
>> tmp9 = runtime_error_constructor(tmp8); // LP8
>> string_delete(tmp8);
>> string_delete(tmp7);
>> string_delete(tmp5);
>> string_delete(tmp4);
>> string_delete(tmp3);
>> string_delete(tmp2);
>> string_delete(tmp1);
>> runtime_cxa_throw(tmp9)
>>
>> LP8: string_delete(tmp8); // Chain to next landing pad
>> LP7: string_delete(tmp7);
>> LP6: string_delete(tmp6);
>> LP5: string_delete(tmp5);
>> LP4: string_delete(tmp4);
>> LP3: string_delete(tmp3);
>> LP2: string_delete(tmp2);
>> LP1: string_delete(tmp1);
>> runtime_rethrow(); // (__UnwindResume) to propagate bad_alloc
>>
>> I omitted a few details for clarity, like the exception handling tables, etc.
>> But the gist of it is that now *at each throw point* you have something like
>> 25+ calls being generated (some are inlined), passing a total of at least 25
>> register args…
>>
>> Well, 10+ times worse? That’s bad enough. But it gets better.
>>
>> Let’s consider runtime cost now. What do these calls do?
>>
>> In case A, three word-size copies in the constructor of WriteError, and I
>> think that’s about it.
>>
>> In case B, each of the strings allocates memory and copies the string in it.
>> Let’s be optimistic and say that it’s one call per allocation, and that you
>> get it from a free list (let’s say something like 5 loads and three stores,
>> that’s rather optimistic). So now we have another 7 calls, and well over 50
>> load/store instructions just for the memory allocation itself. And then the
>> base message is copied at least four times. Cache, cache, cache, buy me more
>> cache!
>>
>> In short, using B looks innocuous, but we replaced, roughly
>> A. two calls, four register args, three memory copies
>> with, at runtime,
>> B. At least 25 calls, at least 25 register args, and four memory copies of
>> the original message.
>>
>> So here too, at least an order of magnitude worse, probably closer to two.
>>
>> Guess what. After writing all this, I realized I had forgotten one
>> concatenation for “: “, so that all my numbers are understimated… Oh well…
>>
>>
>> Meta note: How long do you think it took me to write this reply, in a
>> probably futile attempt to explain that there is more to it than bad_alloc?
>> I’m sorry, but this is really grinding. Now, that is not an entire waste of
>> time if you take the time to really understand what I explained, and if I
>> get the benefit that you never mention bad_alloc ever again ;-)
>>
>> Otherwise, if you only see that as some kind of “justification”, my time is
>> wasted, so is yours, and everybody walks out of it unhappy.
>>
>>
>>>
>>>
>>
>>> Christophe
>>
>>
>
> An improvement to the declaration to avoid some issues could be
>
> template <std::size_t N>
> Error(const char (&msg)[N]) : ...
> {
> …
> }
I actually considered doing something like that, but I find it unnecessarily ugly.
>
> but even so I find that being able to pass a dynamic string adding
> some information is really helpful.
It is. But I want to encourage people to pass that dynamic information in the exception object.
I illustrated that all over the place with IOError/WriteError/ReadError passing errno, etc.
What would be a case where you think that approach would not work?
Is the real problem that my proposal suggests you create a class for that category of dynamic information? If that’s the objection, I’d argue it’s a good thing. It forces you to find commonalities in your code, like I did for WriteError, IIOError and the likes. Among other things, it encourages the good practice of having a shared error message format.
> In many cases you want to add information to the exception and
> is quite common to catch one exception, add another informations
> and throw a more detailed exception.
It is useful, indeed. And when you need to do that, I strongly prefer something like:
catch (WriteError &we) {
if (errno == EPERM) {
collect_SELinuxData(&sedata);
throw WriteErrorWithSELinuxData(we, sedata);
}
}
which I find vastly superior to an alternative like:
catch (some_runtime_error &e) {
if (parse_what_looking_for_errno(e.what()) == EPERM) {
collect_SELinuxData(&sedata);
throw some_runtime_error(e.what() + “ with SELinux information “ + format_se_linux_info_as_string(sedata));
}
>
> Frediano
More information about the Spice-devel
mailing list