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:
end()
etc.Workaround:
If I change my end
method to the following, the failing static_assert
s 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.
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.
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.
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.
If using pointers, always verify if they are NULL before using them.
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).
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).
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With