【问题标题】:Safely check if `this` is null安全地检查 `this` 是否为空
【发布时间】:2021-10-06 22:31:31
【问题描述】:

首先,我知道在空指针上调用方法是未定义的行为。我也知道,因为这不应该发生,编译器可以(并且确实)假设this 始终为非空。

但在实际代码中,您有时会意外地这样做。通常,它没有不良影响,当然this在方法中为null,并且事情可能会崩溃。

作为调试辅助,本着及早崩溃的精神,我将assert(this != 0) 放入了一个我之前不小心调用了几次空指针的方法中。它似乎有效,但clang抱怨:

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 和 clang 现在支持的各种 -fsanitize 选项进行编译。您应该使用assert 获得您所追求的效果,而无需修改您的代码。这并不能回答您的实际问题,但我怀疑您的实际问题没有答案。
  • 在 C++ 中检查指针是否为空的标准方法是 myptr == nullptr;我不知道这是否与 NULL 或 0 不同,但无论如何这是一个常见的检查,例如在进行动态转换以查看它是否成功之后,所以我不认为编译器应该优化它,因为它是非常重要的检查。
  • @TitoneMaurice 当编译器可以在编译时证明比较永远不会为真时,编译器优化与nullptr 的比较是有意义的。这就是编译器真正做的事情。
  • 请注意assert 是一个,仅当NDEBUG 未定义时才有效。大多数 IDE 为发布版本设置了NDEBUG,因此除非您使用使用assert 的调试版本进行全面测试,否则可能无法捕获所有情况。此外,向每个成员函数添加这样的断言并不是 IMO 的好主意。而是尝试添加可以捕获此类错误的综合单元测试(最好不需要assert)。
  • @DragonRock 啊,刚刚做了一个测试,对空指针的方法调用实际上适用于我的编译器。嗯。不知道那个。本来以为在空指针上调用方法会导致运行时错误,但是,是的,既然我这样做了,当然,从编译器的角度来看,对象只是内存中的一些数据。我的错。

标签: c++ undefined-behavior


【解决方案1】:

在 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'

【讨论】:

  • 谢谢,这个功能看起来不错,我去看看!但是,我试图避免显式空检查的开销——主要是通过确保在我不期望的地方没有空。例如。通过让我的函数以合同方式不返回 null。此外,在我的真实代码中,我可能想做的不仅仅是中止......例如打印堆栈跟踪或记录一些东西。
  • 例如,您只能在单元测试中使用这些检查。他们也不应该花费太多开销。据我所知,谷歌在生产中使用了其中一些。
  • @jdm 我看到你说的是clang。也许你应该检查clang.llvm.org/docs/AddressSanitizer.html 我认为它可以提供堆栈跟踪,即使我没有设法让它在我的电脑上工作。对我来说,这个功能看起来像是一个开发功能,一旦你想要 perf,你就禁用它并启用优化,就像你会 NDEBUG(和断言)一样
【解决方案2】:

首先,正如 cmets 对这个问题所指出的:请尝试使用编译器内置的消毒剂。

回答您的问题:您可以将指针转换为 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__);
      }
  }
};

demo

【讨论】:

  • 我怀疑0x10 应该是sizeof(*this)
【解决方案3】:

你应该在调用成员函数之前检查指针,而不是在它之后。

Class *object = ...;
...;
if (object) {
  object->...
}

在成员函数中检查this 是否为nullptr 只能引入调用开销,并且调用者有责任确保this 永远不会是nullptr 或悬空或野指针。 不检查 rhs 是否与赋值运算符中的 lhs 相同的原因。 (但应该确保自赋值正确工作,实现这样的运算符可能会很棘手且容易出错)

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

第一个 ub 检查方法需要的工作量更少,提供的信息也更少。以前的 ub 检查方法需要更多的努力(在任何地方添加断言),但确实提供了更多信息。结果是一个权衡?

如果崩溃可以重现,我更喜欢第一个(不带断言),可以添加一些日志并快速( O(lgn) )找出程序崩溃的行,以及this 的事实nullptr。并找出问题所在。

如果不是这样,那么assert 可能会提供更多信息,但是在这种情况下“取消引用nullptr”是否有很大帮助?我怀疑。

因此,在我看来,正确的方法是构建一个好的日志系统,并使重现错误变得更简单。

【讨论】:

  • "在赋值运算符中不检查 rhs 是否与 lhs 相同的原因。" ——啊?对于所有内置类型和几乎所有标准库类型,给定一个支持赋值类型的对象x,自赋值 (x = x) 是定义良好的,而 x 有效地未修改。可以合理地假设它也为自定义类定义良好。
  • 可能有一些误会。通过“不检查”,我的意思是不要把 if(lhs == rhs) return lhs;在赋值运算符的第一行。但是,赋值运算符不应使程序崩溃,而应始终确保在赋值后 lhs==rhs。
  • 对于自定义赋值运算符的许多(但不是大多数)实现,如果在正文开头没有if (lhs == rhs) return lhs;,则自赋值会导致错误,因为实现通常会假设类不变量在 rhs 上从 body 的开始到结束一直保持不变,但 lhs 上的类不变量可能暂时不成立。如果 lhs 和 rhs 是同一个对象,打破 lhs 上的不变量意味着它们在 rhs 上也被打破。还有其他方法可以避免这种情况,但if (lhs == rhs) 检查也是一种有效的方法。
  • 如果我知道我或我团队中的某个人会意外调用空指针上的方法,我们就不会那样做。但这不是它的工作原理......我可以尝试为每个呼叫站点添加一个检查,但如果我发现我的程序崩溃了,那么我就忘记了。这就是为什么我试图在方法方面捕捉到这一点......我现在所做的不是显式检查,而是试图确保我一开始就不会得到空指针。我尽可能使用引用,或者定义我的函数契约,使它们不会返回 null。
  • 你的意思是,当 lhs rhs 引用同一个对象时,lhs 上的更改(释放成员指针或...)也会影响 rhs(自分配中的常见情况)?
【解决方案4】:

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

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

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

相反,您必须将 nullptr 测试从 this member-function 内联优化中提升出来。为此,您需要提供一个内联的静态(非成员)函数,以便执行您需要的测试。

以下代码满足该要求。

[[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
    }
  }
};

另见:

【讨论】:

  • 具有讽刺意味的是,实现必要语义的唯一方法是强制生成不必要的低效代码。
  • 大声笑,完全同意你的看法。但并不像检查生成的代码那样糟糕。还有其他使用 inline-assembly/GAS 的不太便携的技术。
【解决方案5】:

使用-fno-delete-null-pointer-checks 编译器选项来允许此与空值检查。

【讨论】:

    【解决方案6】:

    也许如果你尝试assert(this != nullptr),但我认为这也会抱怨。您可能想尝试对this 进行类型转换,然后检查该值:

    int* ptr = static_cast<int*>(this);
    

    然后与assert联系。

    【讨论】:

    • static_castint* 不应该编译,reinterpret_castint* 不能处理 OP 的有效担忧,即结果可能不是 nullptr,即使当原来的指针是。
    【解决方案7】:

    clang 警告你是对的 - 如果条件 this == 0 计算结果为真,那么你的程序已经失败并且正在暴露未定义的行为。因此,当您已经知道您的程序不能表现时,您期望 定义 行为(assert() 的正确执行),这有点奇怪。

    听起来像是挂在一根绳子上,你试图在你上方剪断并再次将两端打结......

    如果您想确保您的方法不会被(this == 0) 意外调用,只需将其设为virtual,它将强制转储核心。

    【讨论】:

    • 这嘲笑了 OP 提出了这个问题,甚至没有尝试回答它。即使没有咒骂,那也是毫无意义的粗鲁。
    • 虽然根据标准(我知道)这是未定义的行为,但实际上这不会导致著名的鼻恶魔。相反,x86/x64 上的主要编译器(gcc、clang、msvc)具有可重现的行为,并且显而易见(使用this = null 调用方法)。有很多现有代码依赖于这个事实,包括 MFC。
    • @jdm Even(?) 如果你告诉我“MFC 使用它” - 它仍然是错误的。并且依赖于你使用的编译器的一些特定于实现的运气并不是我所说的安全软件。一旦您尝试使用虚函数执行此操作,您的代码几乎肯定会崩溃。
    • 问题不在于这是“正确”还是“错误”——我不想想要通过空指针调用方法。但它发生在我的代码库中,我想检测它,摆脱它。由于各种原因,我想在方法端检测它(而不是在调用端使用例如-fsanitize)。我认为这是可能的原因是我关心的所有编译器(实际上我见过的每个编译器)都具有相同的特定行为。
    • 我可以忍受在 null 上调用该方法因为我别无选择 - 错误已经存在,我不能只选择不犯错误 :-) .我不是故意这样做的。现在,我发现使用实现定义的行为来检测未定义的行为在道德上并没有错。
    猜你喜欢
    • 1970-01-01
    • 2012-02-29
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-12-16
    • 2022-12-08
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多