c++exceptionc++14stdstd-system-error

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

Solution

  • 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.