`cpp-ub-list` `55.` 空指针解引用
示例
struct T { int i; int foo() { return i; } int bar() { return 0; } static int baz(); }; T* p = nullptr; p->foo(); // Undefined behavior p->bar(); // Undefined behavior p->baz(); // Well-defined, ‘baz’ is a static member
p->baz() 为什么会是良定义?对于内建类型,表达式 E1->E2 与 (*E1).E2 严格等价,任何指针类型都是内建类型。。
这里应该等价为:
(*p).baz();
(*p) 解引用空指针。
这个问题有一定争议,不过在标准中有这样一段话:
ISO/IEC 14882:2017 12.2.2 Non-static member functions(2) If a non-static member function of a class X is called for an object that is not of type X, or of a type derived from X, the behavior is undefined.
这段话将静态成员函数排除在外,言外之意是通过空指针、野指针以及名不符实的对象访问静态成员不是 Undefined,对此一些比较权威的机构也有解释,可以参见:
315. Is call of static member function through null pointer undefined?
232. Is indirection through a null pointer undefined behavior?
等等,就是如果 p 是空指针,*p 又没有被转换为右值,就不应承担作为右值的责任,如:
T* p = nullptr;
*p;
int* q = &(*p);
(*p).static_member();
这些情况都应是合法的。
如果 p 是空指针,*p 又没有被转换为右值
是指?
就是指针指向对象的值没有被用到,就不算解引用
就是指针指向对象的值没有被用到,就不算解引用
那这太夸张了,岂不是空指针访问非静态成员函数也不是 UB 了?
T* p = nullptr; *p; int* q = &(*p); (*p).static_member();这些情况都应是合法的。
这个我表示质疑。
https://eel.is/c++draft/expr.unary.op
一元 * 运算符执行间接操作其操作数应为“指向 T 的指针”类型的 prvalue,其中 T 是对象或函数类型运算符产生 TItype 的左值如果操作数指向对象或函数,则结果表示该对象或函数;否则,除非在 [expr.typeid] 中指定,否则行为是未定义的
The unary * operator performs indirection. Its operand shall be a prvalue of type “pointer to T”, where T is an object or function type. The operator yields an lvalue of type T. If the operand points to an object or function, the result denotes that object or function; otherwise, the behavior is undefined except as specified in [expr.typeid].
@frederick-vs-ja
就是指针指向对象的值没有被用到,就不算解引用
那这太夸张了,岂不是空指针访问非静态成员函数也不是 UB 了?
这确实是不准确的说法,准确的说法在标准中较为分散,我有时间的时候再整理一下,这也确实值得整理
就是指针指向对象的值没有被用到,就不算解引用
那这太夸张了,岂不是空指针访问非静态成员函数也不是 UB 了?
这确实是不准确的说法,准确的说法在标准中较为分散,我有时间的时候再整理一下,这也确实值得整理
OK。
T* p = nullptr; *p; int* q = &(*p); (*p).static_member();这些情况都应是合法的。
这个我表示质疑。
空指针解引用出问题的本质原因在“左值转右值”的过程中,读取错误地址上的数据当然会出问题。
如果 p 是空指针,*p 是左值,但如果 *p 没有被转为右值就不会出问题
*p; 作为单独一个语句的时候不转右值,是没问题的
&(*p) 中的 *p 也不会转为右值,在 C 标准中干脆直说了 &(*p) 和 p 等价,&p[n] 与 p + n 等价
*p = 1; 这种的会出问题,但问题不是出现在 *p 上,而是出现在 = 1 上
这些在标准文档中都有说明,不过得细找找了
T* p = nullptr; *p; int* q = &(*p); (*p).static_member();这些情况都应是合法的。
这个我表示质疑。
空指针解引用出问题的本质原因在“左值转右值”的过程中,读取错误地址上的数据当然会出问题。
如果 p 是空指针,*p 是左值,但如果 *p 没有被转为右值就不会出问题
*p; 作为单独一个语句的时候不转右值,是没问题的
&(*p) 中的 *p 也不会转为右值,在 C 标准中干脆直说了 &(*p) 和 p 等价,&p[n] 与 p + n 等价
*p = 1; 这种的会出问题,但问题不是出现在 *p 上,而是出现在 = 1 上
这些在标准文档中都有说明,不过得细找找了
之前确实有争议,但 https://cplusplus.github.io/CWG/issues/2823.html 目前委员会的态度看起来还是很明确这就是ub。
这个问题有一定争议,不过在标准中有这样一段话:
ISO/IEC 14882:2017 12.2.2 Non-static member functions(2) If a non-static member function of a class X is called for an object that is not of type X, or of a type derived from X, the behavior is undefined.
这段话将静态成员函数排除在外,言外之意是通过空指针、野指针以及名不符实的对象访问静态成员不是 Undefined,对此一些比较权威的机构也有解释,可以参见:
315. Is call of static member function through null pointer undefined?
232. Is indirection through a null pointer undefined behavior?
等等,就是如果 p 是空指针,*p 又没有被转换为右值,就不应承担作为右值的责任,如:
T* p = nullptr; *p; int* q = &(*p); (*p).static_member();这些情况都应是合法的。
如楼上所说,一个最近的缺陷报告已经明确规定了存在 UB。
这个问题有一定争议,不过在标准中有这样一段话:
ISO/IEC 14882:2017 12.2.2 Non-static member functions(2) If a non-static member function of a class X is called for an object that is not of type X, or of a type derived from X, the behavior is undefined.
这段话将静态成员函数排除在外,言外之意是通过空指针、野指针以及名不符实的对象访问静态成员不是 Undefined,对此一些比较权威的机构也有解释,可以参见: 315. Is call of static member function through null pointer undefined? 232. Is indirection through a null pointer undefined behavior? 等等,就是如果 p 是空指针,*p 又没有被转换为右值,就不应承担作为右值的责任,如:
T* p = nullptr; *p; int* q = &(*p); (*p).static_member();这些情况都应是合法的。
如楼上所说,一个最近的缺陷报告已经明确规定了存在 UB。
缺陷报告发出来看看呗~
这个问题有一定争议,不过在标准中有这样一段话:
ISO/IEC 14882:2017 12.2.2 Non-static member functions(2) If a non-static member function of a class X is called for an object that is not of type X, or of a type derived from X, the behavior is undefined.
这段话将静态成员函数排除在外,言外之意是通过空指针、野指针以及名不符实的对象访问静态成员不是 Undefined,对此一些比较权威的机构也有解释,可以参见: 315. Is call of static member function through null pointer undefined? 232. Is indirection through a null pointer undefined behavior? 等等,就是如果 p 是空指针,*p 又没有被转换为右值,就不应承担作为右值的责任,如:
T* p = nullptr; *p; int* q = &(*p); (*p).static_member();这些情况都应是合法的。
如楼上所说,一个最近的缺陷报告已经明确规定了存在 UB。
缺陷报告发出来看看呗~
楼上已经贴出来了
https://cplusplus.github.io/CWG/issues/2823.html
[Accepted as a DR at the November, 2023 meeting.]
正好看到这个讨论,去邮件列表问了一下 CWG 315 和 CWG 2823 的冲突。得到的回复是:
That issue [CWG 315] is more than 20 years old. Meanwhile, the direction of CWG has changed towards a uniform treatment of null pointer values.
目前官方态度是以 CWG 2823 为准,一切对空指针的解引用都是未定义行为(除了提到的 expr.typeid 例外)。
或许 p->baz() 这个情况语义上就不该进行指针解引用。@adah1972 @Mq-b 要不写个 paper 给 EWG ,改掉用 -> 访问静态成员时的等价语义?
@frederick-vs-ja
原始的 CWG 315 就是你说的语义,不真正访问就不算有问题。但现在 CWG 的态度是统一处理。同一封邮件里还写到:
Some people are unhappy with that outcome, but CWG feels a paper to EWG with analysis and rationale is needed to change the situation.
要修改这个行为需要有很强的理由。我目前还想不出什么合法的理由真正需要在空指针上进行这样的操作。
@frederick-vs-ja
原始的 CWG 315 就是你说的语义,不真正访问就不算有问题。但现在 CWG 的态度是统一处理。同一封邮件里还写到:
Some people are unhappy with that outcome, but CWG feels a paper to EWG with analysis and rationale is needed to change the situation.
要修改这个行为需要有很强的理由。我目前还想不出什么合法的理由真正需要在空指针上进行这样的操作。
原本 CWG 315 的描述并不是我说的语义。当时同样要求指针解引用,只是没有说解引用空指针是 UB。 我说的是把这种情况改成根本不进行解引用。理由是没必要进行冗余的操作。
原本 CWG 315 的描述并不是我说的语义。当时同样要求指针解引用,只是没有说解引用空指针是 UB。 我说的是把这种情况改成根本不进行解引用。理由是没必要进行冗余的操作。
我的理解是一回事。原先的语义:不实际会发生的空指针解引用不是未定义行为。现在的语义:只要形式上写出了对空指针解引用,就是未定义行为。你的说法跟原先的语义靠拢。
原先的语义一样有灰色地带:如果一个非静态成员函数不直接或间接访问当前对象的任何数据成员,那我们可以在空指针上调用这个函数吗?实际编译器不会产生有问题的代码,但标准仍认为这是未定义行为,有些工具也能进行告警。
如果要说服标准委员会改变态度,得有一个有说服力的用例,让别人看到有让这个行为定义的必要吧。我没看到。
或许
p->baz()这个情况语义上就不该进行指针解引用。@adah1972 @Mq-b 要不写个 paper 给 EWG ,改掉用->访问静态成员时的等价语义?
关键在于修改的价值,为什么会空指针调用成员函数?如果是静态的,为什么不直接用类名,如果不是静态的,那更就应该让它错。
为了这个问题而修改让 -> 不等价于解引用再 .,有些莫名其妙了。
或许
p->baz()这个情况语义上就不该进行指针解引用。@adah1972 @Mq-b 要不写个 paper 给 EWG ,改掉用->访问静态成员时的等价语义?关键在于修改的价值,为什么会空指针调用成员函数?如果是静态的,为什么不直接用类名,如果不是静态的,那更就应该让它错。
为了这个问题而修改让
->不等价于解引用再.,有些莫名其妙了。
我应该说了:
理由是没必要进行冗余的操作。
理由是没必要进行冗余的操作。
你说的这个“冗余”操作从来就没发生过。这也是之前 CWG315 认为这不是未定义行为的原因。
现在标准我觉得是想把这个问题简化、统一处理。就是没必要对静态成员函数的调用特殊处理。如果只拿实际行为来辩,规则里给出的 p->bar() 也可以是合法的了。
我的理解是一回事。原先的语义:不实际会发生的空指针解引用不是未定义行为。现在的语义:只要形式上写出了对空指针解引用,就是未定义行为。你的说法跟原先的语义靠拢。
原先的语义并不是“不会实际发生的空指针解引用不是未定义行为”,而(可能)是“不通过解引用的结果访问时不产生未定义行为”。解引用一直都是实际发生的。
原先的语义一样有灰色地带:如果一个非静态成员函数不直接或间接访问当前对象的任何数据成员,那我们可以在空指针上调用这个函数吗?实际编译器不会产生有问题的代码,但标准仍认为这是未定义行为,有些工具也能进行告警。
这个 issue 最开始的问题就是涉及静态成员函数,我觉得没有必要提及非静态成员。
解引用一直都是实际发生的。
从编译器产生代码的角度,解引用从来没发生过。从形式的角度,解引用一直存在。CWG 315 原先的合法化方式也是说 *p 没有做 lvalue-to-rvalue 转换就行(实际上也是有点别扭的)。
最关键点我已经说了,只有有人觉得写 p->baz() 需要在 p 为空时合法,认定这很重要,一定要保证它有定义行为,才会有动力去写个提案吧。我看不出目前讨论的人里谁有。
从编译器产生代码的角度,解引用从来没发生过。从形式的角度,解引用一直存在。CWG 315 原先的合法化方式也是说
*p没有做 lvalue-to-rvalue 转换就行(实际上也是有点别扭的)。
但在标准语义中它仍然是一个求值步骤,而且对常量求值是有影响的。
目前各编译器接受这个例子(Godbolt link):
struct S {
static constexpr int value = 42;
};
static_assert([]{
return static_cast<S*>(nullptr)->value;
}() == 42);
而按照当前标准要求这是需要拒绝的。当然这可能是目前实现没有实现 CWG 2823 解决方案带来的副作用。
最关键点我已经说了,只有有人觉得写
p->baz()需要在p为空时合法,认定这很重要,一定要保证它有定义行为,才会有动力去写个提案吧。我看不出目前讨论的人里谁有。
类似地,我觉得“在 baz 在为静态成员函数时,p->baz() 需要在语义上对 p 解引用,这很重要,一定要带来解引用的作用”同样是很奇怪的。
我似乎明白你的意思了。你是说 constexpr 求值里应当没有任何未定义行为这个问题吧?……
我似乎明白你的意思了。你是说 constexpr 求值里应当没有任何未定义行为这个问题吧?……
是的,就是 [expr.const]/5.8 这里。
为达成这条要求,常量求值器在此需要确实地检查指针可解引用(对于 p->baz() 这种情况目前都是没有进行检查)。而如果取消掉 -> 访问静态成员时的指针解引用的话就不应有这种检查了。
对于调用非静态成员函数的例子,即使里面没有进行访问……
struct S {
constexpr int get() const { return 42; }
};
static_assert([]{
return static_cast<S*>(nullptr)->get();
}() == 42);
此时 GCC 与 Clang 是正确拒绝的(Godbolt link)。 (MSVC 目前没有正确拒绝。)
此时 GCC 与 Clang 是正确拒绝的(Godbolt link)。 (MSVC 目前没有正确拒绝。)
你的这个例子在 GCC 13 和最新的 MSVC 下都没有报错。GCC 14 能进行处理,说明在对空指针解引用的解释方面已经有了改变。可以想象,以后通过空指针调用静态成员函数也同样会报错。
还是我前面说的,需要有人觉得写 p->baz() 应该在 p 为空时合法,认定这很重要,一定要保证它有定义行为。你也没这么认为吧。这毕竟是边角情况,没人会正常这么写。让编译器对其报错,对编译器厂商可能是个小小的负担,但看 GCC 从 13 到 14 变化,应该也不算太麻烦。
还是我前面说的,需要有人觉得写
p->baz()应该在p为空时合法,认定这很重要,一定要保证它有定义行为。你也没这么认为吧。
我倒是不这么认为。但是……
- 无论如何设计,常量求值中都会有一种受保证的结果。我想可能可以争议一下“进行较少步骤是更容易正确实现的”,而且似乎理解语义也方便一点。
- 大概 6 年前我在 Qt 中见过这种写法,可能已经改掉了,但我现在不确定。
让编译器对其报错,对编译器厂商可能是个小小的负担,但看 GCC 从 13 到 14 变化,应该也不算太麻烦。
"择日不如撞日“,我给 GCC 提了 issue,目前我的想法是先看下实现者怎么想。