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 takingstd::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.