Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Does std::exception own what?

I'm deriving my own exception, call it MyException, from std::system_error and have overridden what() to calculate and return my message. MyException's initializer list doesn't call the system_error constructor override that takes a message.

If I catch a MyException and copy it to a std::exception the result of calling what() on the std::exception is nullptr. This makes sense.

My question is, if I do use the constructor of system_exception that takes a message when initializing MyException, is it specified that system_error will take a copy of the message and own it and free it?

I'm assuming this would enable a std::exception copy of MyException to be able to return a valid what(). Although I would take a performance hit in that the 'what' needs calculating every time a new one of MyExceptions is created; I can't lazily calculate it only when what() is first called.

I'm slightly worried about the ownership of the 'what' string as what() returns a char* and not a const std::string&.

The code is something like this (I haven't compiled this):

    class MyException : public std::system_error
    {
        std::string what_;
    public:
        MyException(int errorValue, const std::error_category& category)
            : std::system_error(errorValue, category)
        {}

        char* what() const
        {
           what_ = "MyException: " + to_string(code().value());
           return what_.c_str();
        }
    };

    int main()
    {
        std::exception ex;

        try
        {
            throw MyException(4, system_category());
        }
        catch( const MyException& e )
        {
            ex = e;
        }

        printf("what= %s", ex.what());

        return 1;
    }
like image 458
Scott Langham Avatar asked Jul 20 '16 10:07

Scott Langham


2 Answers

My question is, if I do use the constructor of system_exception that takes a message when initializing MyException, is it specified that system_error will take a copy of the message and own it and free it?

Yes, this is guaranteed by the standard.

To start, std::exception does not own whatstd::runtime_error does. std::runtime_error's constructors are defined thusly ([runtime.error]p2-5):

runtime_error(const string& what_arg);

Effects: Constructs an object of class runtime_error.
Postcondition: strcmp(what(), what_arg.c_str()) == 0.

runtime_error(const char* what_arg);

Effects: Constructs an object of class runtime_error.
Postcondition: strcmp(what(), what_arg) == 0.

So, it must store a copy of what_arg internally, as there are no requirements about the lifetime of the value passed in.

Next there's [exception]p2:

Each standard library class T that derives from class exception shall have a publicly accessible copy constructor and a publicly accessible copy assignment operator that do not exit with an exception. These member functions shall meet the following postcondition: If two objects lhs and rhs both have dynamic type T and lhs is a copy of rhs, then strcmp(lhs.what(), rhs.what()) shall equal 0.

So, there must be a copy constructor, it must never throw, and copies must maintain the same return value for what(). Likewise for the copy-assignment operator.

Putting this all together, we can surmise that std::runtime_error must retain the value you pass for what_arg internally in a reference counted string (to avoid exceptions from allocations when copying), and the value will persist regardless of copying and/or slicing – but only down to std::runtime_error, not down to std::exception! (More information about the rationales and requirements regarding what's storage can be found in this very interesting answer by @HowardHinnant: move constructor for std::runtime_error)

std::system_error inherits from std::runtime_error, so all the same holds true for it and any type deriving from it (as long as the derived type maintains the non-throwing copy constructor invariant).

I'm assuming this would enable a std::exception copy of MyException to be able to return a valid what().

No! When you make a std::exception copy of MyException, you are slicing the object down to a less derived type than where what's value is physically stored. If you must make a copy of your exception, the least derived type you can use is std::runtime_error. (You can always safely make a std::exception reference to a MyException, of course.) To put it another way, it is never possible to get a meaningful string from a std::exception object's what().


This code has the behavior you want, portably:

#include <cstdio>
#include <stdexcept>
#include <system_error>
#include <string>

class MyException : public std::system_error {
public:
    MyException(int errorValue, std::error_category const& category)
      : std::system_error(
            errorValue, category,
            "MyException: " + std::to_string(errorValue)
        )
    { }
};

int main() {
    std::runtime_error ex;

    try {
        throw MyException(4, system_category());
    } catch(MyException const& e) {
        ex = e;
    }

    std::printf("what= %s", ex.what());
}

I would say that it's poor form to write an exception constructor that allocates (for obvious reasons), but given that every current standard library implementation that I'm aware of uses short-string optimization for std::basic_string<>, this is extremely unlikely to ever be an issue in practice.

like image 94
ildjarn Avatar answered Nov 15 '22 01:11

ildjarn


Your question is related to understanding the lifecycle of an exception. That question is discussed in posts here and here and may be helpful.

You could guarantee that the lifetime of your exception is extended using a smart pointer. I'm not sure what the performance implications would be, but you could probably use this to hang on to your own extension of the std::system_error and avoid the copy construction altogether. (Actually, I don't guarantee the copy construction would be avoided. Creation of the smart pointer may or may not copy the exception, it seems. But it would copy your exception, which should do the right thing if you provide the copy constructor that you should provide.) Your main function would end up looking more like this.

#include <exception> // std::exception_ptr

int main()
{
    std::exception_ptr p;

    try
    {
        throw MyException(4, system_category());
    }
    catch( const MyException& e )
    {
        p = std::current_exception();
    }

    try
    {
        std::rethrow_exception(p);
    }
    catch (const std::exception& e)
    {
        printf("what= %s", e.what());
    }

    return 1;
}

This is basically just a rewrite of the example of using an exception pointer that I read about on cplusplus.com here, but I've used your exception class rather than a standard exception like std::logic_error.

As to your original question, it seems that it is difficult to make hard guarantees. I did find the following statement in the documentation of the assignment operator for exception as applies to C++11. In C++98 not even this guarantee is provided.

Every exception within the C++ standard library (including this) has, at least, a copy assignment operator overload that preserves the string representation returned by member what when the dynamic types match.

However, the dynamic type of std::system_error would not match the dynamic type of std::exception in your case, so I don't think it would be guaranteed to work.

like image 29
Evan VanderZee Avatar answered Nov 15 '22 00:11

Evan VanderZee