Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is this failing test that adds zero to a null pointer undefined behaviour, a compiler bug, or something else?

Tags:

I wrote a lightweight string_view wrapper for a C++14 project, and with MSVC 2017 it is triggering a static_assert at compile-time, yet the same code at run-time is passes the regular assert. My question is, is this a compiler bug, manifest undefined behaviour, or something else entirely?

Here's the distilled code:

#include <cassert> // assert #include <cstddef> // size_t  class String_View {     char const* m_data;     std::size_t m_size; public:     constexpr String_View()       : m_data( nullptr ),         m_size( 0u )     {}      constexpr char const* begin() const noexcept     { return m_data; }     constexpr char const* end() const noexcept     { return m_data + m_size; } };  void static_foo() {     constexpr String_View sv;  //    static_assert( sv.begin() == sv.end() ); // this errors     static_assert( sv.begin() == nullptr ); //    static_assert( sv.end() == nullptr ); // this errors }  void dynamic_foo() {     String_View const sv;      assert( sv.begin() == sv.end() ); // this compiles & is optimized away     assert( sv.begin() == nullptr );     assert( sv.end() == nullptr ); // this compiles & is optimized away } 

Here's a Compiler Explorer link that I used to replicate the problem.

From what I can tell, adding or subtracting 0 from any pointer value is always valid:

  • c++ - Is the behavior of subtracting two NULL pointers defined? - Stack Overflow, last blockquote
  • Additive operators - cppreference.com, last bullet of the last bullet-list
  • libstdc++: string_view Source File, implementation of end() etc.

Workaround:

If I change my end method to the following, the failing static_asserts will pass.

constexpr char const* end() const noexcept { return ( m_data == nullptr            ? m_data            : m_data + m_size ); } 

Tinkering:

I thought maybe the expression m_data + m_size itself is UB, before the fact that m_size == 0 is evaluated. Yet, if I replace the implementation of end with the nonsensical return m_data + 0;, this still generates the two static_assert errors. :-/

Update:

This does appear to be a compiler bug that was fixed between 15.7 and 15.8.

like image 801
Charles L Wilcox Avatar asked Oct 15 '18 00:10

Charles L Wilcox


People also ask

Is dereferencing a null pointer undefined behavior?

Null dereferencingIn C, dereferencing a null pointer is undefined behavior. Many implementations cause such code to result in the program being halted with an access violation, because the null pointer representation is chosen to be an address that is never allocated by the system for storing objects.

Is 0 a valid pointer?

Since the zero page can be mapped and therefore 0x0 is a valid address, the zero bit pattern should not be used for the null pointer. The null pointer is supposed to be an invalid pointer that never points to any addressable memory.

Why is a pointer with a value of 0 Not a valid address?

It's not even an address abstraction, it's the constant specified by the C standard and the compiler can translate it to some other number as long as it makes sure it never equals a "real" address, and equals other null pointers if 0 is not the best value to use for the platform.

Should you always check if a pointer is null?

If using pointers, always verify if they are NULL before using them.


2 Answers

This looks like an MSVC bug the C++14 draft standard explicitly allows adding and subtracting of the value 0 to a pointer to compare equal to itself, from [expr.add]p7:

If the value 0 is added to or subtracted from a pointer value, the result compares equal to the original pointer value. If two pointers point to the same object or both point one past the end of the same array or both are null, and the two pointers are subtracted, the result compares equal to the value 0 converted to the type std::ptrdiff_t.

It looks like CWG defect 1776 lead to p0137 which adjusted [expr.add]p7 to explicitly say null pointer.

The latest draft made this even more explicit [expr.add]p4:

When an expression J that has integral type is added to or subtracted from an expression P of pointer type, the result has the type of P.
- If P evaluates to a null pointer value and J evaluates to 0, the result is a null pointer value.
- Otherwise, if P points to element x[i] of an array object x with n elements,85 the expressions P + J and J + P (where J has the value j) point to the (possibly-hypothetical) element x[i+j] if 0≤i+j≤n and the expression P - J points to the (possibly-hypothetical) element x[i−j] if 0≤i−j≤n. (4.3).
- Otherwise, the behavior is undefined.

This change was made editorially see this github issue and this PR.

MSVC is inconsistent here in that it allows adding and substracting zero in a constant expression just like gcc and clang does. This is key because undefined behavior in a constant expression is ill-formed and therefore requires a diagnostic. Given the following:

constexpr int *p = nullptr  ; constexpr int z = 0 ; constexpr int *q1 = p + z; constexpr int *q2 = p - z; 

gcc, clang and MSVC allows it a constant expression (live godbolt example) although sadly MSVC is doubly inconsistent in that it allows non-zero value as well, given the following:

constexpr int *p = nullptr  ; constexpr int z = 1 ; constexpr int *q1 = p + z; constexpr int *q2 = p - z; 

both clang and gcc say it is ill-formed while MSVC does not (live godbolt).

like image 60
Shafik Yaghmour Avatar answered Oct 02 '22 07:10

Shafik Yaghmour


I think that this is definitely a bug in the way MSVC evaluates constant expressions, since GCC and Clang have no issues with the code, and the standard is clear that adding 0 to a null pointer yields a null pointer ([expr.add]/7).

like image 44
Brian Bi Avatar answered Oct 02 '22 06:10

Brian Bi