This was originally posted on my blog. I'm posting here to extend its reach. I would love to hear your feedback either here or on my blog
Recently a co-worker asked me a question that I didn’t have a ready answer to:
To facilitate throwing detailed exceptions, I’ve derived several exceptions from std::exception. In order to centralize logging of errors, I created a function that takes a std::exception which I re-throw after logging. Why is the throw statement throwing a std::exception instead of my sub-type? I know the suggested workaround for this is to implement and use raise()
, but that seems like a workaround for why we can’t throw in a polymorphic manner.
Polymorphic Exception Example
The desire that drove this question is to have a central facility to log exceptions. Once logged the exception is re-thrown to the next place able to catch the exception appropriately. The code that my co-worker showed me looks something like this:
#include
#include
class MyException : public std::exception {
public:
virtual const char* what() const throw() {
return "MyException what()";
}
};
void ExceptionLogger(std::exception& e) {
std::cout
C++ Standards Body on Throwing Polymorphically
After my co-worker asked me this I began searching for an answer and, lo and behold, IsoCPP, the website for the C++ standards body, had and answer to how to work around this in their FAQs. The recommended way to do this is to implement your own exception base class and implement a virtual function which throws the current object (typically named raise()
). Here is the example given on the IsoCPP site:
class MyExceptionBase {
public:
virtual void raise();
};
void MyExceptionBase::raise()
{
throw *this;
}
class MyExceptionDerived : public MyExceptionBase {
public:
virtual void raise();
};
void MyExceptionDerived::raise()
{
throw *this;
}
void f(MyExceptionBase& e)
{
// ...
e.raise();
}
void g()
{
MyExceptionDerived e;
try {
f(e);
}
catch (MyExceptionDerived& e) {
// ...code to handle MyExceptionDerived...
}
catch (...) {
// ...code to handle other exceptions...
}
}
As my co-worker stated to me, using raise()
feels like a jarring departure from the normal mechanics of throwing an exception. Because raise()
only needs to throw *this
, it’s even more frustrating because it feels like we’re just moving the same call we would’ve used into a different context than where we wanted to use it. The example, however, works to solve the problem.
Why doesn’t C++ throw polymorphically?
The short explanation given on IsoCPP’s FAQ is that "the statement throw e;
throws an object with the same type as the static type of the expression e". This means that a copy of the object is thrown instead of the original object and, when it is copied, the current type is used. In the example I gave above the type is std::exception. This means that the rest of the exception that is part of the sub-class gets sliced away to use the terminology from IsoCPP.
This, however, didn't fully answer the question for me. It answers what happens not why it happens. It is hard for me to believe that this is an oversight given the lengthy history and use of C++. That left me thinking that the reason for this issue is one of two things:
- It is sticking around for historical reasons and so that it doesn't break lots of existing code.
- There is some performance reason for not having throw act polymorphically.
Finding the answer
I have been writing code in C for many years I have been doing so in the bubble of a single company and its way of doing things. This left me with a deficient knowledge of C++ which I'm currently working to correct. However, because I've been working to correct that gap in my knowledge, I know a few people to go ask!
In order to find out what the answer I went to the C++ Twitter community. Specifically, I reached out to Jason Turner, a host of CppCast, and Shafik Yagmour whose prolific tweet history on C++ speaks for itself. The twitter thread can be seen here.
Polymorphism is Expensive
The answer suggested by Shafik is that, simply put, polymorphism is expensive. There is a long history in the design on the C++ language of not paying for what you don't use. This is done in an effort to make the language efficient. Bjarne Stroustrup details this by page four of a 2012 keynote speech entitled "Foundations of C++". This keynote is a good read in and of itself beyond just helping to answer this question.
My Opinion
I understand the reasoning that polymorphism is expensive and, thus, there are cases where it is beneficial to disallow it. Not only is it expensive it seems to me that code that would be necessary to figure out which subclass to copy could be complicated unless the vtable tells us which specific class we have in the current context. So, not only would the code to figure this out be more expensive, it would be simpler and easier to read if we didn't do it.
That being said, the only case that I can think of where we’re holding a base class object and want to do a deep copy (that is get an object of the derived class from the base class pointer or reference) is in this kind of case where we want to re-throw an exception. My understanding of using a base class to represent a derived class is most useful where we want to do a common operation on all possible derived types. On the other hand I've found somebody asking about this topic outside the scope of exceptions on Stack Overflow. This shows that there is a use for it that I'm unfamiliar. I’m sure that this lack of imagination for a use for this comes from my experience in C where object orientation isn’t available. I'm looking forward to all of you in the C++ community teaching me where this is used.
Lastly, there is an argument to be made that throw should be allowed to throw polymorphically because we’re already in exceptional circumstances. In the normal flow of operation performance is key and polymorphic copying should be something that the programmer has to implement (as shown by raise()
or clone()
in the SO question) only where they really need it. But, if we’ve thrown an exception already, the likelihood is that the program is already in a state where performance is less important than easily preserving information about what happened.
Conclusion
This has been an interesting journey of learning about C++, it's features, and the reasons behind the design of one of the features. While I still think that having to implement your own raise()
or clone()
routine causes mental dissonance in context of throwing exceptions it's not that bad of a solution. I look forward to learning more about this topic. I would love to have you all teach me about this topic and/or show me where to read about it!
Top comments (0)