Richard Hodges
Richard Hodges

Reputation: 69912

c++ deduce the type of a nested exception

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

Answers (3)

Richard Hodges
Richard Hodges

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

Luc Danton
Luc Danton

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)));
}

Live On Coliru

Any time you want more functionality, you can tweak identifiable_base and identifiable_exception to support what you want.

Upvotes: 1

Jarod42
Jarod42

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);
}

Demo

Upvotes: 4

Related Questions