C++ Logo

std-proposals

Advanced search

Re: Stacktrace from exception

From: Andrey Semashev <andrey.semashev_at_[hidden]>
Date: Thu, 29 Apr 2021 12:55:38 +0300
On 4/29/21 11:55 AM, Edward Catmur wrote:
> On Wed, 28 Apr 2021 at 22:54, Andrey Semashev via Std-Proposals
> <std-proposals_at_[hidden] <mailto:std-proposals_at_[hidden]>>
> wrote:
>
> On 4/28/21 11:51 PM, Edward Catmur via Std-Proposals wrote:
> > Absolutely. But bugs occur, and we call third-party library code
> that
> > may also have bugs.
>
> In case of a bug let it crash and collect the stacktraces then
> (possibly, with a core dump). If the bug leads to an exception - debug
> it once (with a debugger, logging, stacktraces - whatever means
> necessary), write a test, and then forget it. You will never see that
> exception again, and you will never need stacktraces automatically
> attached to exceptions.
>
> Crashing is not the desired behavior. If it is possible to shut down
> cleanly (flushing logs, raising incidents, releasing resources) then it
> is better to do so.

When we talk about bugs, the faster you crash the better. As you said
yourself, at this point your data cannot be trusted, and any pending
results may be invalid. So the best course of action is to crash early,
with a core dump, so that the bug is fixed. Graceful termination only
complicates investigation; it doesn't provide you a reliable result of
the operation.

> But normally, an exception is not a bug but rather an exceptional
> situation. As opposed to a bug, an exception is the intended
> reaction to
> some input. Here, a stacktrace usually is not that interesting.
>
> An exception is never the intended reaction to input, because if an
> input is expected it should be handled through normal control flow.

There's different kinds of "expected". There is valid input, which
should be processed normally, and there is invalid input, which should
be indicated as an error or ignored. If you do indicate, an exception is
a very appropriate way to do so.

OTOH, an exception is not a way to indicate bugs. By definition, bugs
are not intended behavior, and you cannot rely on an exception being
thrown somewhere because of a bug. On the contrary, if you throw
exception in some case then *that* is the intended behavior.

> Again, an exception is not a bug. If your system routinely receives
> invalid requests, you want to deal with them fast so that valid
> requests
> still get to be processed.
>
> Exceptions result from a program arriving in an unexpected state; thus
> they are a bug, as we consider it.

No, an exception is a way to signal an exceptional (abnormal, atypical,
unacceptable) situation. That situation is nonetheless expected, hence
the code is written to handle it with an exception being thrown.

> Even if you want to keep the program running (and I do understand there
> are cases where this is appropriate), throwing an exception incurs the
> cost of stack unwinding, locking and unlocking mutexes, calling
> destructors, and (in particular) loading the eh tables from a cold
> section, which may be way out in L1 or even require a page fault to load
> from disk. Is the overhead of a maybe 80 byte allocation really
> noticeable next to that?

Yes. The amount of work that is done during the stack unwinding is
dynamic and may not involve anything you listed. And that work is
necessary to continue operation (i.e. catch the exception, process it
and go on). Capturing a stacktrace likely has larger algorithmic
complexity (as it has to unwind beyond the exception handler) and it is
not necessary to continue operation. It may improve diagnostics, yes,
but it is not necessary for the program to continue running.

And allocating memory during an exception throw is also not good in its
own right. There is a case of an exception being thrown in low memory
conditions.

> > I'm opposed to doing this *implicitly* and *by default*. On the
> > contrary, I would welcome the feature if it was defined as
> explicitly
> > requested by the user.
> >
> > Also fine - as long as all of std::current_exception_stacktrace()
> > std::current_exception(), and throw; constitute an explicit request.
>
> The stacktrace must have been collected by the time when these
> functions
> are called. So no, they don't count as a request.
>
> The compiler can determine whether those could be called by the handler,
> and suppress stacktrace if they are not.

In general, no it can't. Static code analysis doesn't work when you need
a definitive answer because the compiler may not see the whole program.

> > > Well, obviously you'd use both std::nested_exception and
> > > Boost.Exception, not to mention your own exception hierarchy
> > inheriting
> > > from both. And std::exception is polymorphic, as is
> > boost::exception, so
> > > almost all of the time (except with some really old libraries)
> > there's
> > > no problem using dynamic_cast.
> >
> > Most types that you'd want to attach won't be polymorphic.
> >
> > Sure, so you dump them into your holds-anything exception object
> that is
> > mixed with std::nested_exception.
>
> Again, why would you mess with std::nested_exception?
>
> When you are at a point to attach data to an exception, you already have
> an exception_ptr from the original throw site.

No, you're either throwing an exception or you're catching it. In both
cases you have the exception type (or one of its base classes). If you
want to attach more data to it, you just operate on the exception
itself, exception_ptr is not involved.

The only case when you'd use std::nested_exception is when you catch one
exception (say, from library A), and you want to transform it to your
own domain (say, that of your library B), and you want to retain the
original for better diagnostics or special handling in the caller. Then
you would wrap the original exception in std::nested_exception and throw
it as your exception of domain B. Note that the caller would be catching
exception of domain B, he will not be able to catch an exception of
domain A. And that is exactly what is intended in this case.

It is a completely different story when you have an exception and you
just want to augment it with additional data. The caller will still be
catching your one and only exception, but he will also be able to
extract that additional data.

> > > The point of std::nested_exception is more that it allows
> you to
> > attach
> > > your own exception hierarchy to an existing exception of
> > arbitrary type.
> >
> > And why would you do that? I mean, unless you do literally have
> > multiple
> > exceptions that you want to chain together, why would you
> want to wrap
> > your data, like std::stacktrace for example, into another
> exception
> > type, just to use it with std::nested_exception? Which is not
> designed
> > to work as a container of arbitrary data in the first place
> to boot?
> >
> > No, there's no need to pull an owl on a globe since
> boost::exception
> > can
> > already be a base class for an exception, and provides the
> necessary
> > infrastructure for adding and extracting the attached data.
> >
> > Every library has its own exception hierarchy; hopefully they're
> > polymorphic and with luck they are based on std::exception or
> > boost::exception. But if they aren't, you have to deal with them
> anyway.
>
> I think, you missed my point. You don't attach exceptions, you attach
> arbitrary data that is not an exception to an exception. Furthermore,
> you may attach the same kind of data to different types of exceptions.
> And preferably you shouldn't be required to write an exception class
> for
> every such combination.
>
> Yes, that's why you use a holds-anything exception class. But you also
> need to mix in the original exception, wherever that was thrown from.

If you are converting a foreign exception to your own one that has this
boost::exception capability then sure, you could retain the original in
std::nested_exception. But that act of conversion/retaining is not
inherent to the use case of attaching data to exceptions. Put another
way, you can attach data without having another exception that you want
to keep chained. And in fact, you are converting exceptions far less
often than you throw or rethrow your own. If your exceptions are
boost::exception-enabled, you may just not have the use for
std::nested_exception at all.

> > Exceptions are never going to be efficient or elegant; whatever
> you can
> > get to work is good enough. As for attaching data,
> boost::exception is
> > nice enough, but it doesn't provide the necessary fine-grained
> access to
> > individual data items; all you can do is convert them to string
> > representation en bloc.
>
> Oh, no, you very much can access individual items, that's the point.
>
> https://www.boost.org/doc/libs/1_76_0/libs/exception/doc/get_error_info.html
> <https://www.boost.org/doc/libs/1_76_0/libs/exception/doc/get_error_info.html>
>
> The ability to format all attached data along with the exception itself
> (https://www.boost.org/doc/libs/1_76_0/libs/exception/doc/diagnostic_information.html
> <https://www.boost.org/doc/libs/1_76_0/libs/exception/doc/diagnostic_information.html>)
>
> is just a cherry on top.
>
> That only works if you know the type of the item. There's no way to
> iterate over items of unknown type individually.

Ok, but do you need to iterate? In order to process the attached data,
you do need to know its type anyway, so I see no problem here.

Received on 2021-04-29 04:55:45