首先,我知道在空指针上调用方法是未定义的行为。我也知道,因为这不应该发生,编译器可以(并且确实)假设this
总是非空的。
但在实际代码中,您有时会意外地执行此操作。通常,它没有不良影响,当然this
在方法中为空,并且事情可能会崩溃。
作为调试辅助工具,本着崩溃早期的精神,我assert(this != 0)
放入了我之前几次意外调用空指针的方法中。它似乎有效,但叮当抱怨:
warning: 'this' pointer cannot be null in well-defined C++ code; comparison may be
assumed to always evaluate to true [-Wtautological-undefined-compare]
assert (this ! = 0);
^~~~ ~
我想知道检测this
为空的最佳(最少错误)方法是什么。一个简单的比较可以优化出来。
- 我可以对
this
进行一些指针算术,以尝试欺骗编译器,或强制它将指针视为整数。 - 我可以使用
memcmp
. - 也许有特定于编译器的扩展说"不要优化这个表达式"?
另一个问题是,在继承的情况下,这个指针的"null"实际上可能类似于0x00000004
,所以也处理这种情况会很好。我对Clang,MSVC或GCC的解决方案感兴趣。
在 gcc 中,您可以使用-fsanitize=null
构建。Clang也应该有这个选项。
从man gcc
:
-fsanitize=null
This option enables pointer checking. Particularly, the application built with this option turned on will issue an error message when it tries to dereference a NULL pointer, or if a
reference (possibly an rvalue reference) is bound to a NULL pointer, or if a method is invoked on an object pointed by a NULL pointer.
这是一个测试程序:
[ ~]$ cat 40783056.cpp
struct A {
void f() {}
};
int main() {
A* a = nullptr;
a->f();
}
[ ~]$ g++ -fsanitize=null 40783056.cpp
[ ~]$
[ ~]$ ./a.out
40783056.cpp:7:7: runtime error: member call on null pointer of type 'struct A'
使用-fno-delete-null-pointer-checks
编译器选项允许此检查与空检查。
首先,正如对这个问题的评论所指出的那样:请尝试使用编译器内置的消毒器。
要回答您的问题:您可以将指针投射到uintptr_t
并比较该值。请注意,此转换是实现定义的,不能保证按预期工作。
以下代码在 clang 3.9 中没有优化任何内容,但在 gcc 7.0 中删除了空指针检查。0x10
是任意选择的。
struct foo
{
void bar() {
if(this == nullptr) {
sink(__LINE__);
}
if((uintptr_t)this < 0x10) {
sink(__LINE__);
}
if((volatile uintptr_t)this < 0x10) {
sink(__LINE__);
}
}
};
演示
你应该在调用成员函数之前检查指针,而不是在它之后。
Class *object = ...;
...;
if (object) {
object->...
}
在成员函数中检查this
是否为 nullptr 只会引入调用开销,并且调用者应始终负责确保this
永远不会是 nullptr 或悬空或野生指针。 不检查 rhs 是否与赋值运算符中的 lhs 相同的原因相同。(但是应该确保自赋值正常工作,实现这样的运算符可能很棘手且容易出错)
当this
nullptr 为 nullptr 时尝试访问成员函数或变量通常会立即使程序崩溃,而取消引用 nullptr 实际上是一个 ub。当this==nullptr时断言(this!=nullptr)也会立即终止你的程序,但提供比崩溃更多的信息,崩溃也依赖于ub。
第一种 ub 检查方法需要更少的努力,并且提供的信息更少。以前的 ub 检查方法需要更多的努力(在任何地方添加断言),但确实提供了更多信息。结果是取舍?
如果崩溃可以重现,我更喜欢第一个(没有断言),可以添加一些日志并快速( O(lgn))找出哪一行使程序崩溃,以及this
nullptr
的事实。并找出哪个调用有问题。
否则,assert
可能会提供更多信息,但是在这种情况下,"取消引用nullptr
"是否有很大帮助?我怀疑。
因此,在我看来,正确的方法是构建一个良好的日志记录系统,并简化重现错误的过程。
根据您的编译器,它可能会积极地强制执行关于this
是未定义行为的nullptr
的C++规则。
但是,优化的发布应用程序/代码仍然需要能够优雅地处理和报告可能发生这种情况的场景;在某些情况下,它实际上可能是优化性能的有意设计的一部分(JIT 引擎 - 例如我工作的那些)。
在这种情况下,您需要通过清楚地告知编译器您的意图来与编译器携手合作。this
上任何类型的演员表,并试图与0
或nullptr
的任何变体进行比较,都不能保证可靠地工作。通常,编译器会抱怨或优化它,因为它在优化的版本中无效。
相反,您必须将nullptr
测试提升到this
成员函数内联优化之外。为此,您需要提供一个不受内联约束的静态(非成员)函数,以便执行所需的测试。
以下代码满足该要求。
[[gnu::noinline]] // Required for testing `this` == nullptr;else c++/clang optimizes away
static bool af_isnullptr(void* self) { return self == nullptr; }
然后,您可以在class
或struct
中按如下方式使用它:
class Example {
void member() {
if(af_isnullptr(this)) {
// handle `this == nullptr` case
}
}
};
另请参阅:
- C++核心准则评论
[[gsl::not_null]]
属性意图。[[gsl::strict_not_null]]
属性意图。
也许如果你尝试assert(this != nullptr)
但我认为这也会抱怨。您可能需要尝试对this
进行类型转换,然后检查该值:
int* ptr = static_cast<int*>(this);
然后与assert
核实。
clang
警告你是正确的 - 如果条件this == 0
计算为 true,则你的程序已经向南移动并且正在暴露未定义的行为。因此,当您已经知道程序不能预期运行时,您期望定义的行为(正确执行assert()
),这有点奇怪。
听起来就像挂在绳子上,你试图在你上方剪断并再次将两端打结在一起......
如果你想确保你的方法不会被意外调用(this == 0)
,只需让它virtual
,它就会强制转储核心。