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

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

本文关键字:编译器 错误 其他 什么 空指针 测试 失败 添加 未定义      更新时间:2023-10-16

我为 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
}

这是我用来复制问题的编译器资源管理器链接。

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

  • c++ - 是否定义了减去两个 NULL 指针的行为? - 堆栈溢出,最后一个块引用
  • 添加剂运算符 - cppreference.com,最后一个项目符号列表的最后一个项目符号
  • libstdc++:string_view源文件,end()的实现等。

解决方法:

如果我将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错误 C++14 草案标准明确允许在指针0添加和减去值以从 [expr.add]p7 中等于自身进行比较:

如果在指针值

中添加或减去值 0,则结果将等于原始指针值。如果两个指针指向同一对象,或者两个指针都指向同一数组末尾的一个指针,或者两个指针都为 null,并且减去这两个指针,则结果将等于转换为类型 std::p trdiff_t 的值 0。

看起来 CWG 缺陷 1776 导致 p0137 调整 [expr.add]p7 以明确表示null pointer

最新的草案对此更加明确[expr.add]p4:

当将整型表达式 J 添加到指针类型的表达式 P 或从中减去时,结果的类型为 P.- 如果 P 的计算结果为空指针值,
J 的计算结果为 0,则结果为空指针值。
- 否则,如果 P 指向具有 n 个元素的数组对象 x 的元素 x[i],85 表达式 P + J 和 J + P(其中 J 具有值 j(指向(可能假设的(元素 x[i+j] 如果 0≤i+j≤n 和表达式 P - J 指向(可能假设的(元素 x[i−j] 如果为 0≤i−j≤n。 (4.3).
- 否则,行为是未定义的。

此更改是在编辑中看到此 github 问题和此 PR 的。

MSVC 在这里不一致,因为它允许在常量表达式中添加和减去零,就像 gcc 和 clang 所做的那样。这是关键,因为常量表达式中的未定义行为格式不正确,因此需要诊断。鉴于以下情况:

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

gcc,clang和MSVC允许它一个常量表达式(实时Godbolt示例(,尽管遗憾的是MSVC是双重不一致的,因为它也允许非零值,给定以下内容:

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

Clang和GCC都说它是畸形的,而MSVC则没有(Live Godbolt(。

我认为这绝对是 MSVC 计算常量表达式方式中的一个错误,因为 GCC 和 Clang 对代码没有问题,并且标准很清楚,将 0 添加到空指针会产生空指针 ([expr.add]/7(。