这个失败的测试是将零添加到空指针未定义行为、编译器错误还是其他原因?

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

我为 C++14 项目写了一个轻量级的 string_view 包装器,在 MSVC 2017 中它在编译时触发 static_assert,但在 运行-时间是通过常规assert。我的问题是,这是一个编译器错误、明显的未定义行为,还是完全是其他原因?

这是提炼代码:

#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
}

这是我用来重现问题的Compiler Explorer link

据我所知,从任何指针值中添加或减去 0 始终有效:

解决方法:

如果我将 end 方法更改为以下方法,失败的 static_assert 将通过。

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

修补:

我认为表达式 m_data + m_size 本身可能是 UB,在 m_size == 0 被求值之前。然而,如果我用无意义的 return m_data + 0; 替换 end 的实现,这仍然会产生两个 static_assert 错误。 :-/

更新:

这似乎是一个编译器错误,已在 15.7 和 15.8 之间修复。

我认为这绝对是 MSVC 评估常量表达式的方式中的一个错误,因为 GCC 和 Clang 的代码没有问题,而且标准很明确,向空指针添加 0 会产生一个空指针 ([ expr.add]/7).

这看起来像一个 MSVC 错误,C++14 草案标准明确允许将值 0 添加和减去一个指针以比较等于自身,从 [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.

看起来 CWG defect 1776 lead to p0137 which adjusted [expr.add]p7 明确地说 null pointer

最新草案使这一点更加明确[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 github issue and this PR

MSVC 在这里不一致,因为它允许在常量表达式中添加和减去零,就像 gcc 和 clang 一样。这是关键,因为 undefined behavior in a constant expression is ill-formed 因此需要诊断。鉴于以下内容:

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

gcc、clang 和 MSVC 允许它使用常量表达式 (live godbolt example),尽管遗憾的是 MSVC 是双重不一致的,因为它也允许非零值,给定以下条件:

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

clang 和 gcc 都说它格式错误,而 MSVC 则没有 (live godbolt)。