-
Notifications
You must be signed in to change notification settings - Fork 290
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
cpp-ub-list
55.
空指针解引用
#52
Comments
这个问题有一定争议,不过在标准中有这样一段话:
这段话将静态成员函数排除在外,言外之意是通过空指针、野指针以及名不符实的对象访问静态成员不是 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://eel.is/c++draft/expr.unary.op 一元 * 运算符执行间接操作其操作数应为“指向 T 的指针”类型的 prvalue,其中 T 是对象或函数类型运算符产生 TItype 的左值如果操作数指向对象或函数,则结果表示该对象或函数;否则,除非在 [expr.typeid] 中指定,否则行为是未定义的
|
这确实是不准确的说法,准确的说法在标准中较为分散,我有时间的时候再整理一下,这也确实值得整理 |
OK。 |
空指针解引用出问题的本质原因在“左值转右值”的过程中,读取错误地址上的数据当然会出问题。 如果 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。 |
缺陷报告发出来看看呗~ |
楼上已经贴出来了
|
正好看到这个讨论,去邮件列表问了一下 CWG 315 和 CWG 2823 的冲突。得到的回复是:
目前官方态度是以 CWG 2823 为准,一切对空指针的解引用都是未定义行为(除了提到的 expr.typeid 例外)。 |
原始的 CWG 315 就是你说的语义,不真正访问就不算有问题。但现在 CWG 的态度是统一处理。同一封邮件里还写到:
要修改这个行为需要有很强的理由。我目前还想不出什么合法的理由真正需要在空指针上进行这样的操作。 |
原本 CWG 315 的描述并不是我说的语义。当时同样要求指针解引用,只是没有说解引用空指针是 UB。 |
我的理解是一回事。原先的语义:不实际会发生的空指针解引用不是未定义行为。现在的语义:只要形式上写出了对空指针解引用,就是未定义行为。你的说法跟原先的语义靠拢。 原先的语义一样有灰色地带:如果一个非静态成员函数不直接或间接访问当前对象的任何数据成员,那我们可以在空指针上调用这个函数吗?实际编译器不会产生有问题的代码,但标准仍认为这是未定义行为,有些工具也能进行告警。 如果要说服标准委员会改变态度,得有一个有说服力的用例,让别人看到有让这个行为定义的必要吧。我没看到。 |
你说的这个“冗余”操作从来就没发生过。这也是之前 CWG315 认为这不是未定义行为的原因。 现在标准我觉得是想把这个问题简化、统一处理。就是没必要对静态成员函数的调用特殊处理。如果只拿实际行为来辩,规则里给出的 |
原先的语义并不是“不会实际发生的空指针解引用不是未定义行为”,而(可能)是“不通过解引用的结果访问时不产生未定义行为”。解引用一直都是实际发生的。
这个 issue 最开始的问题就是涉及静态成员函数,我觉得没有必要提及非静态成员。 |
从编译器产生代码的角度,解引用从来没发生过。从形式的角度,解引用一直存在。CWG 315 原先的合法化方式也是说 最关键点我已经说了,只有有人觉得写 |
但在标准语义中它仍然是一个求值步骤,而且对常量求值是有影响的。 目前各编译器接受这个例子(Godbolt link): struct S {
static constexpr int value = 42;
};
static_assert([]{
return static_cast<S*>(nullptr)->value;
}() == 42); 而按照当前标准要求这是需要拒绝的。当然这可能是目前实现没有实现 CWG 2823 解决方案带来的副作用。
类似地,我觉得“在 |
我似乎明白你的意思了。你是说 constexpr 求值里应当没有任何未定义行为这个问题吧?…… |
是的,就是 [expr.const]/5.8 这里。 为达成这条要求,常量求值器在此需要确实地检查指针可解引用(对于 对于调用非静态成员函数的例子,即使里面没有进行访问…… struct S {
constexpr int get() const { return 42; }
};
static_assert([]{
return static_cast<S*>(nullptr)->get();
}() == 42); 此时 GCC 与 Clang 是正确拒绝的(Godbolt link)。 |
你的这个例子在 GCC 13 和最新的 MSVC 下都没有报错。GCC 14 能进行处理,说明在对空指针解引用的解释方面已经有了改变。可以想象,以后通过空指针调用静态成员函数也同样会报错。 还是我前面说的,需要有人觉得写 |
我倒是不这么认为。但是……
"择日不如撞日“,我给 GCC 提了 issue,目前我的想法是先看下实现者怎么想。 |
C++ 标准邮件列表上的回复很清楚,就是所有这三种情况都是未定义行为,且需要按照 [expr.const]/5.8 在 constexpr 求值时报失败。 |
p->baz()
为什么会是良定义?对于内建类型,表达式E1->E2
与(*E1).E2
严格等价,任何指针类型都是内建类型。。这里应该等价为:
(*p)
解引用空指针。The text was updated successfully, but these errors were encountered: