Construct standard exceptions with null pointer argument and impossible postconditions

Viewed 393

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.

3 Answers

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

Yes, this is undefined behavior. [res.on.arguments] says:

1.1 - If an argument to a function has an invalid value (such as [...] a pointer invalid for its intended use), the behavior is undefined.

Clearly checking postconditions is an intended use of a function (or constructor) argument.

Is a null pointer invalid here as an argument to strcmp? [cstring.syn] refers us to the C library; referring to the appropriate Standard (specifically C17 according to [intro.refs], not that it makes a difference which version of the C standard we consult), in 7.1.4 Use of library functions we read:

1 - [...] If an argument to a function has an invalid value (such as [...] a null pointer [...]) [...] the behavior is undefined.

Then, 7.24.1 String function conventions says:

1 - The header <string.h> declares [...] several functions [...] useful for manipulating arrays of character type and other objects treated as arrays of character type. [...] in all cases a char * or void * argument points to the initial (lowest addressed) character of the array.

And 7.24.4.2 The strcmp function says:

2 - The strcmp function compares the string pointed to by s1 to the string pointed to by s2.

And, circling back to 7.1.1 Definitions of terms, we read:

1 - A string is a contiguous sequence of characters terminated by and including the first null character. [...] A pointer to a string is a pointer to its initial (lowest addressed) character.

So a null pointer is an invalid argument to strcmp, so checking the postcondition has undefined behavior, so the null pointer is invalid for its intended use as a constructor argument to std::range_error, so certainly the program has undefined behavior; both compilers are conformant since anything can happen when undefined behavior is encountered at runtime.

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.

Related