问题
Consider the following program:
#include<stdexcept>
#include<iostream>
int main() {
try {
throw std::range_error(nullptr);
} catch(const std::range_error&) {
std::cout << "Caught!\n";
}
}
GCC and Clang with libstdc++ call std::terminate
and abort the program with the message
terminate called after throwing an instance of 'std::logic_error'
what(): basic_string::_S_construct null not valid
Clang with libc++ segfaults on construction of the exception.
See godbolt.
Are the compilers behaving standard-conform? The relevant section of the standard [diagnostics.range.error] (C++17 N4659) does say that std::range_error
has a const char*
constructor overload which should be preferred over the const std::string&
overload. The section also does not state any preconditions on the constructor and only states the postcondition
Postconditions:
strcmp(what(), what_arg) == 0
.
This postcondition always has undefined behavior if what_arg
is a null pointer, so does this mean that my program also has undefined behavior and that both compilers act conformant? If not, how should one read such impossible postconditions in the standard?
On second thought, I think it must mean undefined behavior for my program, because if it didn't then (valid) pointers not pointing to null-terminated strings would also be allowed, which clearly makes no sense.
So, assuming that is true, I would like to focus the question more on how the standard implies this undefined behavior. Does it follow from the impossibility of the postcondition that the call also has undefined behavior or was the precondition simply forgotten?
Inspired by this question.
回答1:
From the doc:
Because copying std::range_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.
This shows why you get segfault, the api does really treat it as a real string.
In general in cpp if something was optional, there will be an overloaded constructor/function that does not take what it does not need. So passing nullptr
for a function that does not document something to be optional is going to be undefined behavior. Usually API's don't take pointers with the exception for C strings. So IMHO it is safe to assume passing nullptr for a function that expects a const char *
, is going to be undefined behavior. Newer APIs may prefer std::string_view
for those cases.
Update:
Usually it is fair to assume a C++ API taking a pointer to accept NULL. However C strings are a special case. Until std::string_view
, there was no better way to pass them efficiently. In general for an API accepting const char *
, assumption should be is that it has to be a valid C string. i.e a pointer to a sequence of char
s that terminates with a '\0'.
range_error
could validate that the pointer is not nullptr
but it cannot validate if it terminates with a '\0'. So it better not do any validation.
I don't know the exact wording in the standard, but this pre-condition is probably assumed automatically.
回答2:
This goes back to the basic question is it OK to create a std::string from nullptr? and what should it do it is?
www.cplusplus.com says
If s is a null pointer, if n == npos, or if the range specified by [first,last) is not valid, it causes undefined behavior.
So when
throw std::range_error(nullptr);
is called the implementation tries to make something like
store = std::make_shared<std::string>(nullptr);
which is undefined. Which I would consider a bug (without having read the actual wording in the standard). Instead the libery developers could have made something like
if (what_arg)
store = std::make_shared<std::string>(nullptr);
But then the catcher has to check for nullptr in what();
or it will just crash there. So the std::range_error
should assign either an empty string or "(nullptr)" like some other languages does.
来源:https://stackoverflow.com/questions/60924295/construct-standard-exceptions-with-null-pointer-argument-and-impossible-postcond