Reputation: 69912
Introduction:
given:
struct X : std::runtime_error {
using std::runtime_error::runtime_error;
};
When we call std::throw_with_nested(X("foo"))
, what is actually thrown is not an X
. It is some type that is derived from both X
and std::nested_exception
.
therefore, the following assertion will fail:
const std::type_info *a = nullptr, *b = nullptr;
try
{
throw X("1");
}
catch(X& x) {
a = std::addressof(typeid(x));
try {
std::throw_with_nested(X("2"));
}
catch(X& x) {
b = std::addressof(typeid(x));
}
}
assert(std::string(a->name()) == std::string(b->name()));
What I would like to do is deduce that these two exceptions are related.
First attempt:
std::type_index
deduce_exception_type(const std::exception* pe)
{
if (auto pnested = dynamic_cast<const std::nested_exception*>(pe))
{
try {
std::rethrow_exception(pnested->nested_ptr());
}
catch(const std::exception& e)
{
return deduce_exception_type(std::addressof(e));
}
}
else {
return typeid(*pe);
}
}
This fails because std::nested_exception::nested_ptr()
returns a pointer to the next exception down the line, not the X
interface of the current exception.
I'm looking for (portable) ideas and solutions that allow me to recover the typeid(X) from the 'exception with unknown name' thrown by the standard library during std::rethrow_exception
.
c++14 and c++1z are fine.
Why?:
Because I want to be able to unwrap a complete exception hierarchy and transmit it across an rpc session, complete with exception type names.
I ideally don't want to have to write a catch block featuring every exception type in the system, which would have to be weakly ordered by derivation depth.
A further example of expected functionality (and an illustration of why my approach does not work):
const std::type_info *b = nullptr;
try
{
throw std::runtime_error("1");
}
catch(std::exception&) {
try {
std::throw_with_nested(X("2"));
}
catch(X& x) {
// PROBLEM HERE <<== X& catches a std::_1::__nested<X>, which
// is derived from X and std::nested_exception
b = std::addressof(typeid(x));
}
}
assert(std::string(typeid(X).name()) == std::string(b->name()));
Upvotes: 10
Views: 1228
Reputation: 69912
Thanks to the guys who responded.
In the end I felt the most reliable way was to demangle the result of typeid::name()
and remove any "nested" parts of the typename.
I did have a got at building an exception-registration map, but this then required non-standard throw and rethrow mechanisms to hook into the map.
It's a little platform-specific but it can be encapsulated in a library function:
#include <regex>
#include <string>
namespace
{
std::string remove_nested(std::string demangled)
{
#if _LIBCPP_VERSION
static const std::regex re("^std::__nested<(.*)>$");
#elif __GLIBCXX__
static const std::regex re("^std::_Nested_exception<(.*)>$");
#endif
std::smatch match;
if (std::regex_match(demangled, match, re))
{
demangled = match[1].str();
}
return demangled;
}
}
my use case (Exception
is derived from google::protobuf::Message
):
void populate(Exception& emsg, const std::exception& e)
{
emsg.set_what(e.what());
emsg.set_name(remove_nested(demangle(typeid(e))));
try {
std::rethrow_if_nested(e);
}
catch(std::exception& e)
{
auto pnext = emsg.mutable_nested();
populate(*pnext, e);
}
catch(...) {
auto pnext = emsg.mutable_nested();
pnext->set_what("unknown error");
pnext->set_name("unknown");
}
}
where demangle()
again is defined in terms of platform-specific code. In my case:
demangled_string demangle(const char* name)
{
using namespace std::string_literals;
int status = -4;
demangled_string::ptr_type ptr {
abi::__cxa_demangle(name, nullptr, nullptr, &status),
std::free
};
if (status == 0) return { std::move(ptr) };
switch(status)
{
case -1: throw std::bad_alloc();
case -2: {
std::string msg = "invalid mangled name~";
msg += name;
auto p = (char*)std::malloc(msg.length() + 1);
strcpy(p, msg.c_str());
return demangled_string::ptr_type { p, std::free };
}
case -3:
assert(!"invalid argument sent to __cxa_demangle");
throw std::logic_error("invalid argument sent to __cxa_demangle");
default:
assert(!"PANIC! unexpected return value");
throw std::logic_error("PANIC! unexpected return value");
}
}
demangled_string demangle(const std::type_info& type)
{
return demangle(type.name());
}
Where demangled_string
is a convenient wrapper around the memory returned from abi::__cxa_demangle
(or similar in windows):
struct demangled_string
{
using ptr_type = std::unique_ptr<char, void(*)(void*)>;
demangled_string(ptr_type&& ptr) noexcept;
const char* c_str() const;
operator std::string() const;
std::ostream& write(std::ostream& os) const;
private:
ptr_type _ptr;
};
demangled_string::demangled_string(ptr_type&& ptr) noexcept
: _ptr(std::move(ptr))
{}
std::ostream& demangled_string::write(std::ostream& os) const
{
if (_ptr) {
return os << _ptr.get();
}
else {
return os << "{nullptr}";
}
}
const char* demangled_string::c_str() const
{
if (!_ptr)
{
throw std::logic_error("demangled_string - zombie object");
}
else {
return _ptr.get();
}
}
demangled_string::operator std::string() const {
return std::string(c_str());
}
Upvotes: 1
Reputation: 35459
One way around is to consistently use your own throw_with_nested
, wherein you inject the functionality you want:
#include <typeinfo>
#include <exception>
struct identifiable_base {
virtual std::type_info const& type_info() const = 0;
};
template<typename Exception>
struct identifiable_exception: Exception, identifiable_base {
using Exception::Exception;
explicit identifiable_exception(Exception base)
: Exception(std::move(base))
{}
std::type_info const& type_info() const override
{
// N.B.: this is a static use of typeid
return typeid(Exception);
}
};
template<typename Exception>
identifiable_exception<std::decay_t<Exception>> make_identifiable_exception(Exception&& exception)
{ return identifiable_exception<std::decay_t<Exception>> { std::forward<Exception>(exception) }; }
// N.B.: declared with a different name than std::throw_with_nested to avoid ADL mistakes
template<typename Exception>
[[noreturn]] void throw_with_nested_identifiable(Exception&& exception)
{
std::throw_with_nested(make_identifiable_exception(std::forward<Exception>(exception)));
}
Any time you want more functionality, you can tweak identifiable_base
and identifiable_exception
to support what you want.
Upvotes: 1
Reputation: 218118
Adapted print_exception
from http://en.cppreference.com/w/cpp/error/nested_exception :
const std::type_info&
deduce_exception_type(const std::exception& e)
{
try {
std::rethrow_if_nested(e);
} catch(const std::exception& inner_e) {
return deduce_exception_type(inner_e);
} catch(...) {
}
return typeid(e);
}
Upvotes: 4