How to construct a or excepti

2020-02-12 06:31发布

问题:

Exceptions defined in <stdexcept> (e.g. std::logic_error, std::runtime_error and their subclasses such as std::system_error) have constructors expecting string arguments, e.g.:

domain_error(const string& what_arg);
domain_error(const char* what_arg);

with postconditions

strcmp(what(), what_arg.c_str()) == 0
strcmp(what(), what_arg) == 0

respectively. There is no requirement that these arguments passed to the constructors remain valid during the lifetime of these exceptions, so the only way to ensure that the postconditions hold, is to duplicate and store these dynamic strings. This requires memory, so I assume that their construction itself may throw std::bad_alloc or similar, which is usually most unexpected. This causes problems, because every code example I've seen in the wild encourages people to write code like

if (haveError)
    throw std::runtime_error("BOO!"); // May throw std::bad_alloc instead?!

whereas it would seem to be much safer to construct the exception beforehand in some other place, e.g:

struct A {
    // During allocation of A one would often expect std::bad_alloc anyway:
    A() : m_someException("BOO!") {}
    void f() {
        /* Do stuff */
        if (haveError)
            throw m_someException;
            /* Note that according to §18.8.1.2 all standard library
               classes deriving from `std::exception` must have publicly
               accessible copy constructors and copy assignment operators
               that do not exit with an exception. In implementations such
               exception instances most likely share the common string
               with all their copies. */
    }
    std::runtime_error const m_someException;
};

This makes me very cautious of libraries which throw any such exceptions, e.g even regex_error from <regex> in C++11!!!

Why don't these exceptions have no-throw/noexcept constructors? Does the C++ core guidelines have a say on this?

PS: Personally I would have left what() a pure abstract method at this point in the exception ancestry chain.

EDIT 09.10.2017: Here's a PoC demonstrating that std::runtime_error construction can throw a std::bad_alloc instead:

#include <cstddef>
#include <cstdlib>
#include <new>
#include <stdexcept>
#include <string>

bool throwOnAllocate = false;

void * operator new(std::size_t size) {
    if (!throwOnAllocate)
        if (void * const r = std::malloc(size))
            return r;
    throw std::bad_alloc();
}

void operator delete(void * ptr) { std::free(ptr); }

int main() {
    std::string const errorMessage("OH NOEZ! =(");
    throwOnAllocate = true;
    throw std::runtime_error(errorMessage);
}

回答1:

Short answer, it is not possible to construct any objects with an absolute guarantee of no exceptions.

You may consider allocating on the stack, but your thread stack could run out and will cause system error/exception. Your CPU may get an external interrupt, right when you throw, that system can't handle and everything gets fried. As others have suggested, don't fret the small stuff. Running out of memory is something most user programs can't recover from, so don't worry about it, elegantly fail. Don't try to handle every bad situation, just ones you can easily recover from.

As a side-note, for the situation about running out of memory, a lot of high graphics games do all of their heap allocation up-front during game initialization and try to avoid after the game starts to reduce running into issues with out of memory/slow allocations in the middle of a game (jittery game & bad user experience). You can similarly be smart about design of your program to reduce the chances of running into bad situations.



回答2:

You cannot construct std::logic_error or std::runtime_error without ever getting std::bad_alloc, but most of the time it doesn't matter. If the operation can fail, separate code path has to be provided anyway to handle this, same code path can be used to handle std::bad_alloc as well. In those rare cases when it matters, you should just be derived from std::exception directly and make what() member function return a fixed string. Most of the time "function X throws Y", should be read as "function X throws Y and std::bad_alloc", unless explicitly stated otherwise. This also why C++11 discarded throw() specified in favor of noexcept().

I don't find the up-front allocation of exceptions helpful, since if you are encountering std::bad_alloc, losing some error information at this point might be a good choice as such situations are rare and not worth the hassle. Calling code can just assume such function has failed due to memory allocation failure as part of its normal work.

Now if you are worried about losing error information because of exception is thrown during handling of another exception, you can try looking at exception chaining (or exception nesting). Examples of this can be found in other languages:

  • Python has the best implementation of this and implements both explicit and implicit exception chaining: https://www.python.org/dev/peps/pep-3134/
  • Java implements explicit and partially implicit chaining: http://docs.oracle.com/javase/7/docs/api/java/lang/Throwable.html (look for getCause() and getSuppressed())
  • C# implements only explicit exception chaining: https://msdn.microsoft.com/en-us/library/system.exception.innerexception(v=vs.110).aspx

C++11 provides standard way of nesting exceptions with std::throw_with_nested() and std::rethrow_if_nested(). Unfortunately, if you throw exception outside of catch() block, using std::rethrow_if_nested() later on that exception will terminate your program, so this facility IMO is kinda broken. If you really care about such issues, you can implement your own variant that can do both explicit and implicit chaining (you will need std::current_exception() to do this). You won't be able to force external libraries to use your facility, but at least your code can be very advanced in this regard.



回答3:

if (haveError)
    throw std::runtime_error("BOO!"); // May throw std::bad_alloc instead?!

by the time you reach this throw you should have already taken care of all clean up so in most cases, having a std::bad_alloc thrown instead of a std::runtime_error won't make much of a difference really.

The only exceptional case, I can thing of is when exceptions are used to control the flow of a program - I tend to do this very often with code such as:

try { 
  auto face = detectFace(); // may throw a custom no_face_exception or a many_faces_exception
  // do work with face
} 
catch (no_face_exception& e) {
  std::cout << "no face detected\n";
}
catch (many_faces_exception& e) {
  std::cout << "too many faces detected\n";
}

In this particular case failure to allocate memory would cause detectFace to throw a std::bad_alloc instead which would cause a disastrous crash. First allocating the exceptions before throwing, as you suggested, would not change anything at all - the program would still crash with a std::bad_alloc since the allocation would still fail. The only way around this problem would be to simply catch the std::bad_alloc:

catch (std::bad_alloc& e) {
  std::cout << "bad alloc reported\n";
}


回答4:

Your problem is true only if you are trying to use built-in exception types. Although what some other answers in this thread say, there is a difference between throwing and failing, and there is a difference between throwing different types of exceptions.

How can I say that? Because this is how the exceptions mechanism is built for.

Failing to allocate on the stack provide no easy recovery while failing to allocate on the heap just throws. There is a difference because there is a way to recover from heap failure. In the same way, one can catch different exceptions because one might deal differently with them. There is such a mechanism because recovery might be very different.

Moreover, while bad_alloc might not be very common on most platforms and applications, it is something that one should take care of, especially in those cases where memory is small and/or application requires lots of it. And yes, recovery from bad_alloc might sometimes be very different than recovery from other issues, like database errors or connection issues. It is true, though, that if you get the bad_alloc when trying to allocate only enough room for a very short message, you are on a deep problem, but still there might be recovery option (e.g.: If you hold lots of data for caching purpose only, you might release them).

Nevertheless, the solution is quite simple - it is not a coincidence that the what() function of std::exception is virtual, and it returns the simplest type - const char *. You can inherit any exception you want directly from std::exception, and implement it in a way that does not allocate on the heap. For example, have the message as a static member of your class (possibly with some placeholders for values) or just provide it as a string literal. That way you can make sure that no new allocation is to be done upon throwing.