I want my pony! Or why you cannot have C++ exceptions with a stack trace
Many modern languages such as Java and C# provide the capability, once an exception is generated, to retrieve the stack trace of all the invocation chain from the starting point (usually the main() but it can be the starting point of the thread) to the exact point in which the exception has been thrown.
For quite a long time I have been a big fan of such feature and I implemented that in C++ using the backtrace() function in Unix or the CaptureStackBackTrace() in Win32. Those implementations required a bunch of tricks such as wrapping the throw statement in a macro, write some non-portable code to extract the back trace and to demangle the symbols in order for me, inside the catch statement to access the trace and being able to give it a sense.
Here, I will not present the implementation of the stack trace capture to be used in C++ but I will explain why such a thing was both a waste of time and useless. To explain why I arrived to such conclusions, we need to understand the exceptions system of C++.
Exceptions in C++ are more expensive than in other languages. Not more expensive in general terms but in relative ones. Both Java and C# have a lot of extra checks and controls that make programming safer. For this reason, everything in Java and in C# costs a bit more than in C++. Keeping this in mind, in Java and C#, exceptions are not that expensive with respect to the rest of what the language does to be safe and consistent. On the other hand, C++ has been designed to be lightweight and to make you pay only for the things that you use. In C++ you can screw up memory at any time because there are no checks unless you use some specific methods/functions, e.g., the at(size_type) method in the interface of the +std::vector<T>+ checks if the access is performed inside the bounds the underlying array but operator(size_type) does not. If you want, you can use the second one to prevent paying the checks that the first one implements. Moreover, since throwing an exception triggers a bunch of actions during the stack unrolling, like invoking the the destructor of all the objects that has been created up to the point in which we are able to catch the exception, invoking the destructor methods can imply flushing streams and freeing memory which can be expensive as well. In Java and C#, when you unwind the stack, you just forget about the objects that you created on the way because the garbage collector will take care of them and, as a consequence of this the stack unwinding, is relatively lighter in C# and Java than in C++.
On the tragicomic side of things, in C++, the stack trace could represent a code path that you never directly created. While in Java and in C#, the code is loaded in the virtual machine, it passes through a step of just in time compilation which transforms and optimizes the code to be executed on the real machine and finally the generated code is executed by the processor. During such an optimisation step, methods can be inlined and disappear but when you throw the exception, you can always look at the stack that is in the virtual machine and refer to that in order to show a backtrace that will actually make sense to the user. In C++, all the optimizations are done at compile time: if something got inlined, it did at that time and the inlined methods are "lost" in the final executable. This means that, when you walk back the stack, the stack can be composed by smaller amount of methods than the ones that would have been there without the inlining. The result of this is that the stack trace that you would observe could not respect what you logically expected to see looking at the source code.
Another thing is that the moment in which you throw and catch an exception, is the very moment in which you do not need to have a stack trace. In C++, exceptions are used in really exceptional situations such as "memory is over mate!" which usually is represented by a std::bad_alloc exception. In this specific case, it is not the place where you threw the exception the place where the problem is but, more probably, a bunch of actions that the program did before getting there consumed all the memory. The same applies for the other — very few indeed — exception types that C++ provides. For instance, if you get a std::bad_cast exception it is because somewhere in the program, the application passed and object of the wrong type to a series of functions that ended up doing a dynamic_cast that has nothing else to do than giving up and express its disappointment throwing an exception. Usually in C++ the exception are caught only in order to give applications — or particular code paths — a graceful shutdown making sure that the application is able to flush all the buffers, tell about the kind of error that triggered the failure and to give a hint — pointing to the right try block — on where the problem was noticed. To reinforce the concept: usually, in C++, the exception are not really actionable. If the application eats all the memory, there is not very much it can do at run time but exiting crying and stepping its feet.
Moreover, and this is the reason why I do not present any code to implement the back trace in C++, when you need to collect the stack frames, you usually need to allocate a piece of memory in the heap, because you cannot put that on a stack that you are going to be popped immediately after. Unfortunately, there is no guarantee on that the allocation will be even possible. If the exception is meant to signal the fact that the memory is finished, where are we allocating the stack trace? On the same line, the C++ standard only tells you that the exceptions can always be instantiated, meaning that they live in a separate and reserved piece of memory where there is no guarantee for you to be able to store even the pointer to the back trace.
To recap, if you want to implement your own exceptions in C++ with the stack trace in the same way as Java or C# do, that's good fun and you can learn something more about the API of your favourite system and compiler but, if you plan using them to do something useful, it is not probably worth the time you will spend. That being said, have fun and thanks for reading! 😊