Although this solution conflicts with your third requirement 3: No Catch and rethrow, I propose an solution with std::nested_exception
and macros because this seems to provide a reasonable solution for the current problem at least to me.
I hope this too long answer can help you.
1. An Error Handling with std::nested_exception
First, we can recursively nest exceptions using std::nested_exception
.
Roughly speaking, We can add exceptions of arbitrary types to this class by calling std::throw_with_nested
.
These enables us to carry all information of thrown exceptions with a rather simple code, just throwing each exception by std::throw_with_nested
in each ellipsis catch handler catch(…){ }
in the upper level.
For instance, following function h
throws a std::nested_exception
which aggregates the user-defined exception SomeException
and std::runtime_error
:
struct SomeException : public std::logic_error {
SomeException(const std::string& message) : std::logic_error(message) {}
};
[[noreturn]] void f(){
std::throw_with_nested(SomeException("error."));
}
[[noreturn]] void g()
{
try {
f();
}
catch (...) {
std::throw_with_nested(std::runtime_error("Error of f."));
}
};
[[noreturn]] void h()
{
try {
g();
}
catch (...) {
std::throw_with_nested(std::runtime_error("Error of g."));
}
}
Locating Exceptions (Fundamental Level)
Replacing all of these std::throw_with_nested
by the following function throw_with_nested_wrapper
through the macro THROW_WITH_NESTED
, we can make records of the file names and the line numbers where exceptions occurred.
As is well known, __FILE__
and __LINE__
are pre-defined by C++ standard.
So the macro THROW_WITH_NESTED
has a key role to add these location information:
// "..." are arguments of the ctor of ETYPE
// and the first one must be a string literal.
#define THROW_WITH_NESTED(ETYPE, ...) \
throw_with_nested_wrapper<ETYPE>(__FILE__, __LINE__, __VA_ARGS__);
template<typename E, typename ...Args>
[[noreturn]]
void throw_with_nested_wrapper(
char const* fileName,
std::size_t line,
const std::string& message,
Args&& ...args)
{
auto info = std::string(fileName)
+ ", l." + std::to_string(line) + ", "
+ message;
std::throw_with_nested(E(info, std::forward<decltype(args)>(args)...));
};
Locating Exceptions (Upper Level)
If we have to pick up information about where the exception was triggered in the upper level, the following macro HOOK
reusing the above macro THROW_WITH_NESTED
would work for us:
#define HOOK(OPERATION) \
[&]() \
{ \
try{ \
return OPERATION; \
} \
catch(...){ \
auto info = std::string(#OPERATION) + ", upper level."; \
THROW_WITH_NESTED(std::runtime_error, info); \
} \
}()
Finally, first three functions f
, g
and h
are rewritten and simplified as follows:
[[noreturn]] void f(){
THROW_WITH_NESTED(SomeException, "SomeException, fundamental level.");
}
void g(){
HOOK(f());
};
void h(){
HOOK(g());
}
Extracting Error Information
Extracting all explanatory information of nested exceptions is a simple task.
Passing the caught exception at the most outer try-catch block into the following function output_exceptions_impl
, we can do it.
Each nested exception can be recursively thrown by std::nested_exception::rethrow_nested
.
Since this member function calls std::terminate
when there is no stored exception, we should apply dynamic_cast
to avoid it as pointed out in this post:
template<typename E>
std::enable_if_t<std::is_polymorphic<E>::value>
rethrow_if_nested_ptr(const E& exception)
{
const auto *p =
dynamic_cast<const std::nested_exception*>(std::addressof(exception));
if (p && p->nested_ptr()){
p->rethrow_nested();
}
}
void output_exceptions_impl(
const std::exception& exception,
std::ostream& stream,
bool isFirstCall = false)
{
try
{
if (isFirstCall) { throw; }
stream << exception.what() << std::endl;
rethrow_if_nested_ptr(exception);
}
catch (const std::runtime_error& e) {
stream << "Runtime error: ";
output_exceptions_impl(e, stream);
}
/* ...add further catch-sections here... */
catch(...){
stream << "Unknown Error.";
}
}
BTW, explicit try-catch
blocks at the most outer places are rather verbose and thus I usually use the following macro proposed in this post:
#define CATCH_BEGIN try{
#define CATCH_END(OSTREAM) } catch(...) { output_exceptions(OSTREAM); }
void output_exceptions(std::ostream& stream)
{
try {
throw;
}
catch (const std::exception& e) {
output_exceptions_impl(e, stream, true);
}
catch (...) {
stream << "Error: Non-STL exceptions." << std::endl;
}
}
Then all exceptions thrown from h
can be traced and printed by the following code.
Inserting the macros THROW_WITH_NESTED
, HOOK
, CATCH_BEGIN
and CATCH_END
at the right lines of our code, we can locate exceptions in each thread:
CATCH_BEGIN // most outer try-catch block in each thread
...
HOOK(h());
...
CATCH_END(std::cout)
Then we get the following output where file names and line numbers are just an examples.
All the available information is recorded:
DEMO with 2 threads
Runtime error: prog.cc, l.119, h(), upper level.
Runtime error: prog.cc, l.113, g(), upper level.
Runtime error: prog.cc, l.109, f(), upper level.
Logic error: prog.cc, l.105, SomeException, fundamental level.
2. In case of FooResouce
First requirement is
- Pass some optional context information as an extra argument to the Ctor, which can then be stored in the exception
Let us define the following special exception class SomeException
which contains some optional context information and the member function getContext
to get it:
class SomeException : public std::runtime_error
{
std::string mContext;
public:
SomeException(
const std::string& message,
const std::string& context)
: std::runtime_error(message), mContext(context)
{}
const std::string& getContext() const noexcept{
return mContext;
}
};
Adding a new argument context
to FooResouce::FooResouce
and replacing throw
by THROW_WITH_NESTED
, we can pass the first requirement within the above error handling framework:
class FooResouce
{
public:
FooResouce(
T arg_to_construct_with,
const std::string& context)
{
m_foo = create_foo_resouce(arg_to_construct_with);
if(!m_foo){
THROW_WITH_NESTED(SomeException, "Ctor failed.", context);
}
...
}
...
};
Next,
but we have no information about where the exception was triggered in the upper level. Here upper level refers the function that tried to create a FooResource,
Creating each FooResource
with HOOK
, we can get information about where the ctor was failed in the upper level.
The caller side would be as follows.
In this manner, all error information including messages, contexts and their locations would be clarified in each thread.
CATCH_BEGIN // most outer try-catch block in each thread
...
auto resource = HOOK(FooResouce(T(), "context"));
...
CATCH_END(std::cout)
Finally,
- At callsite use a pushContext function. This function will store the context using thread-local storage.
Although I don't know the detail of this requirement, but since we can call SomeException::getContext
in output_exceptions_impl
as follows and get all contexts from every thrown SomethingExceptions
, I think we can also store them like this:
DEMO(my proposal)
void output_exceptions_impl(
const std::exception& exception,
std::ostream& stream,
bool isFirstCall = false)
{
...
catch (const SomeException& e) { // This section is added.
stream
<< "SomeException error: context:"
<< e.getContext() << ", "; // or pushContext?
output_exceptions_impl(e, stream);
}
...
}