25

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:

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.

Charles L Wilcox
  • 1,126
  • 8
  • 18
  • 1
    This bug doesn't reproduce in the current release version VS 15.8.7 - the program compiles fine with both of the commented lines uncommented. – Casey Oct 17 '18 at 14:33
  • @Casey That's great to know! My IDE is VS2017 15.3, using the MSVC++ 14.11 toolchain. – Charles L Wilcox Oct 17 '18 at 17:28

2 Answers2

14

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

Shafik Yaghmour
  • 154,301
  • 39
  • 440
  • 740
  • There are some cases In C and I think C++ where the actual value of a "constant" expression of pointer type may not be resolvable until link time. Is the C++ Standard clear on whether the use of comparison or difference operators on such pointers is required to be `constexpr`? – supercat Oct 15 '18 at 15:47
11

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

Brian Bi
  • 111,498
  • 10
  • 176
  • 312