安全地检查"this"是否为空



首先,我知道在空指针上调用方法是未定义的行为。我也知道,因为这不应该发生,编译器可以(并且确实)假设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 相同的原因相同。(但是应该确保自赋值正常工作,实现这样的运算符可能很棘手且容易出错)

thisnullptr 为 nullptr 时尝试访问成员函数或变量通常会立即使程序崩溃,而取消引用 nullptr 实际上是一个 ub。当this==nullptr时断言(this!=nullptr)也会立即终止你的程序,但提供比崩溃更多的信息,崩溃也依赖于ub。

第一种 ub 检查方法需要更少的努力,并且提供的信息更少。以前的 ub 检查方法需要更多的努力(在任何地方添加断言),但确实提供了更多信息。结果是取舍?

如果崩溃可以重现,我更喜欢第一个(没有断言),可以添加一些日志并快速( O(lgn))找出哪一行使程序崩溃,以及thisnullptr的事实。并找出哪个调用有问题。

否则,assert可能会提供更多信息,但是在这种情况下,"取消引用nullptr"是否有很大帮助?我怀疑。

因此,在我看来,正确的方法是构建一个良好的日志记录系统,并简化重现错误的过程。

根据您的编译器,它可能会积极地强制执行关于this是未定义行为的nullptr的C++规则。

但是,优化的发布应用程序/代码仍然需要能够优雅地处理和报告可能发生这种情况的场景;在某些情况下,它实际上可能是优化性能的有意设计的一部分(JIT 引擎 - 例如我工作的那些)。

在这种情况下,您需要通过清楚地告知编译器您的意图来与编译器携手合作。this上任何类型的演员表,并试图与0nullptr的任何变体进行比较,都不能保证可靠地工作。通常,编译器会抱怨或优化它,因为它在优化的版本中无效。

相反,您必须将nullptr测试提升到this成员函数内联优化之外。为此,您需要提供一个不受内联约束的静态(非成员)函数,以便执行所需的测试。

以下代码满足该要求。

[[gnu::noinline]] // Required for testing `this` == nullptr;else c++/clang optimizes away
static bool af_isnullptr(void* self) { return self == nullptr; }

然后,您可以在classstruct中按如下方式使用它:

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,它就会强制转储核心。

相关内容

最新更新