Home > front end >  Is throwing a temporary value as reference undefined behavior?
Is throwing a temporary value as reference undefined behavior?

Time:06-18

To my surprise, std::runtime_error only has a const std::string& constructor, but no value constructor. Thus I am wondering if throw std::runtime_error("Temporary" std::to_string(100)); is defined. After all we are creating an exception object that refers to a temporary object inside the scope of a function that immediately exits (as we throw out of it). Further investigation showed that while returning a constant reference to a temporary value immediately causes a segfault on my system, throwing one doesn't:


const std::string& undefined_behavior() {
    return "Undefined "   std::to_string(1);
}

void test() {
    throw std::runtime_error("Hello : "   std::to_string(2));   
}

void test2() {
    const std::string& ref = "Hello : "   std::to_string(3); 
    throw ref;
}

int main(int argc, char** argv) {
    //std::cout << undefined_behavior() << std::endl;
    try {
        test(); 
    } catch(const std::runtime_error& ex) {
        std::cout << ex.what() << std::endl;
    }
    try {
        test2();
    } catch(const std::string& ref) {
        std::cout << ref << std::endl;
    }
}

On my system, undefined_behavior() crashes immediately but test() and test2() run fine.

While I know the call to undefined_behavior() is undefined behavior, is test() and test2() undefined? If not, do thrown values get a specfied treatment?

And if they are all undefined but happened to work on my computer by accident, what is the proper way to throw an exception with a variable message?

CodePudding user response:

The const std::string& constructor doesn't cause the exception object to store a reference to the passed std::string. std::runtime_error will make an internal copy of the passed string (although the copy will be stored in an usual way, probably reference-counted in an allocation external to the exception object; this is to give better exception guarantees when the exception object is copied, which the standard requires).

A function taking a const lvalue reference doesn't usually mean that it takes ownership or stores a reference to the object.

Prior to C 11 there was no reason to not use a const reference here, since it was always more efficient than passing by-value. With C 11 move semantics that changed and it is true that usually these kind of constructors had rvalue reference variants added with C 11, so that passing a temporary std::string object to the constructor would allow more efficient move-construction instead of copy-construction.

My guess is that this simply never was done for the exception classes because the performance of their construction should not be important in comparison to the overhead of actually throwing the exception.

So in test()'s case a temporary string object is created from the operation, which is passed to the constructor and the exception object will make a copy of it. After the initialization of the exception object the temporary string is destroyed, but that is not a problem, since the exception object stores/references a copy.

In test2() the same would apply if you had written throw std::runtime_error(ref). However, here a reference is bound immediately to the temporary object materialized from the return value of . This causes its lifetime to be extended to that of the reference variable. Therefore again, the temporary lives long enough for the std::runtime_error to make a copy from it.

But since you write simply throw ref that is not exactly what happens. throw ref will throw a std::string, because that is the type of the ref expression. However, throw doesn't actually use the object referenced by the expression as exception object. The exception object is initialized from the operand of the throw expression. So here, the exception object will be another std::string object (in unspecified storage) which will be initialized from the temporary that ref refers to.

undefined_behavior() has undefined behavior when the return value is used, because the temporary std::string object materialized from the return value and which the return value of undefined_behavior() references is not lifetime-extended. Although it is again immediately bound by a reference, the lifetime-extension rule specifically doesn't apply to objects bound to a reference in a return value initialization. Instead the temporary is destroyed when the function returns, causing the function to always return a dangling reference.

CodePudding user response:

It is not undefined behavior.


In test1(), the constructor of std::runtime_error makes an internal copy. There is even a note on this at cppreference:

Because copying std::runtime_error is not permitted to throw exceptions, this message is typically stored internally as a separately-allocated reference-counted string. This is also why there is no constructor taking std::string&&: it would have to copy the content anyway.


In test2() there is no std::runtime_error involved. Instead, as you can read e.g. here, in the following statement

throw expr;

the exception object is copy-initialized from expr. So there is a copy made of that temporary string also in this case.

  • Related