【问题标题】:Checking if this is null检查这是否为空
【发布时间】:2010-12-23 01:47:13
【问题描述】:

检查 this 是否为空有意义吗?

假设我有一个带有方法的类;在该方法中,我检查this == NULL,如果是,则返回错误代码。

如果 this 为 null,则表示该对象已被删除。该方法甚至能够返回任何东西吗?

更新:我忘了提到该方法可以从多个线程调用,它可能会导致对象被删除,而另一个线程在该方法中。

【问题讨论】:

  • 这并不意味着该对象已被删除。删除指针不会自动将其归零,((Foo*)0)->foo() 是完全有效的语法。只要foo() 不是虚函数,它甚至可以在大多数编译器上工作,但它就是 icky。
  • “可能会导致对象被删除,而另一个线程在方法中”。这是不可接受的,您不能删除对象,而其他代码(在同一线程或不同线程中)保留它将使用的引用。它也不会导致 this 在其他线程中变为 null。
  • 我现在正在研究一种情况,这绝对是 NULL。只有上帝知道为什么。
  • "this" 似乎在父类是抽象类时为空。
  • 来自 GCC 6.2 发行说明:值范围传播现在假定 C++ 成员函数的 this 指针不为空。

标签: c++ pointers null


【解决方案1】:

检查 this==null 是否有意义?我在进行代码审查时发现了这一点。

在标准 C++ 中,它不会,因为对空指针的任何调用已经是未定义的行为,因此任何依赖于此类检查的代码都是非标准的(甚至无法保证检查会被执行)。

请注意,这也适用于非虚拟函数。

但是,某些实现允许this==0,因此专门为这些实现编写的库有时会将其用作 hack。这种组合的一个很好的例子是 VC++ 和 MFC - 我不记得确切的代码,但我清楚地记得在某处看到 if (this == NULL) 检查 MFC 源代码。

它也可能作为调试帮助,因为在过去的某个时候,由于调用者中的错误,此代码被this==0 命中,因此插入了一个检查以捕获未来的实例。不过,对于此类事情,断言会更有意义。

如果 this == null 则表示该对象已被删除。

不,不是那个意思。这意味着在空指针或从空指针获得的引用上调用了一个方法(尽管获得这样的引用已经是 U.B.)。这与delete 无关,并且不需要任何此类对象曾经存在过。

【讨论】:

  • 对于delete,通常情况正好相反——如果你delete一个对象,它不会被设置为NULL,然后你尝试调用它的方法,你'会找到this != NULL,但如果它的内存已经被其他对象回收使用,它可能会崩溃或行为异常。
  • delete 可能并不总是得到左值,请考虑 delete this + 0;
  • Visual Studio 6 于 1998 年 6 月发布。C++ 标准于 9 月发布。好的,所以微软本可以预料到该标准,但原则上许多预标准编译器没有实现该标准并不奇怪;-)
  • 只是为了好玩,这是一篇关于 MFC 的好文章,它使用了 if (this == 0) viva64.com/en/b/0226
  • 请注意,MFC 是一种特殊情况,因为它从未真正打算与 VC++ 以外的任何东西一起使用,而且由于团队保持联系,MFC 团队可以依赖于实现细节,例如这(并确保它们会在需要的时候存在,并且完全按照需要运行)。由于即使对于 VC++ 也没有记录或保证此行为,因此第三方库不能依赖它。
【解决方案2】:

您关于线程的注释令人担忧。我很确定您有可能导致崩溃的竞争条件。如果一个线程删除一个对象并将指针归零,另一个线程可以在这两个操作之间通过该指针进行调用,导致this 为非空且无效,从而导致崩溃。同样,如果一个线程调用一个方法,而另一个线程正在创建对象,你也可能会遇到崩溃。

简短的回答,您确实需要使用互斥锁或其他东西来同步对该变量的访问。您需要确保this 从不为空,否则您将遇到问题。

【讨论】:

  • “你需要确保这永远不会为空” - 我认为更好的方法是确保operator-> 的左操作数永远不会为空:) 但除此之外,我希望我可以+10 这个。
【解决方案3】:

我知道这是旧的,但我觉得现在我们正在处理 C++11-17,有人应该提到 lambdas。 如果您将 this 捕获到稍后将异步调用的 lambda 中,则您的“this”对象可能会在该 lambda 被调用之前被销毁。

即将它作为回调传递给一些耗时的函数,该函数从单独的线程运行或通常只是异步运行

编辑:为了清楚起见,问题是“检查这是否为空有意义吗”我只是提供了一个场景,它确实有意义,随着现代 C++ 的广泛使用可能会变得更加普遍。

人为的例子: 这段代码是完全可运行的。要查看不安全行为,只需注释掉对安全行为的调用并取消注释不安全行为调用。

#include <memory>
#include <functional>
#include <iostream>
#include <future>

class SomeAPI
{
public:
    SomeAPI() = default;

    void DoWork(std::function<void(int)> cb)
    {
        DoAsync(cb);
    }

private:
    void DoAsync(std::function<void(int)> cb)
    {
        std::cout << "SomeAPI about to do async work\n";
        m_future = std::async(std::launch::async, [](auto cb)
        {
            std::cout << "Async thread sleeping 10 seconds (Doing work).\n";
            std::this_thread::sleep_for(std::chrono::seconds{ 10 });
            // Do a bunch of work and set a status indicating success or failure.
            // Assume 0 is success.
            int status = 0;
            std::cout << "Executing callback.\n";
            cb(status);
            std::cout << "Callback Executed.\n";
        }, cb);
    };
    std::future<void> m_future;
};

class SomeOtherClass
{
public:
    void SetSuccess(int success) { m_success = success; }
private:
    bool m_success = false;
};
class SomeClass : public std::enable_shared_from_this<SomeClass>
{
public:
    SomeClass(SomeAPI* api)
        : m_api(api)
    {
    }

    void DoWorkUnsafe()
    {
        std::cout << "DoWorkUnsafe about to pass callback to async executer.\n";
        // Call DoWork on the API.
        // DoWork takes some time.
        // When DoWork is finished, it calls the callback that we sent in.
        m_api->DoWork([this](int status)
        {
            // Undefined behavior
            m_value = 17;
            // Crash
            m_data->SetSuccess(true);
            ReportSuccess();
        });
    }

    void DoWorkSafe()
    {
        // Create a weak point from a shared pointer to this.
        std::weak_ptr<SomeClass> this_ = shared_from_this();
        std::cout << "DoWorkSafe about to pass callback to async executer.\n";
        // Capture the weak pointer.
        m_api->DoWork([this_](int status)
        {
            // Test the weak pointer.
            if (auto sp = this_.lock())
            {
                std::cout << "Async work finished.\n";
                // If its good, then we are still alive and safe to execute on this.
                sp->m_value = 17;
                sp->m_data->SetSuccess(true);
                sp->ReportSuccess();
            }
        });
    }
private:
    void ReportSuccess()
    {
        // Tell everyone who cares that a thing has succeeded.
    };

    SomeAPI* m_api;
    std::shared_ptr<SomeOtherClass> m_data = std::shared_ptr<SomeOtherClass>();
    int m_value;
};

int main()
{
    std::shared_ptr<SomeAPI> api = std::make_shared<SomeAPI>();
    std::shared_ptr<SomeClass> someClass = std::make_shared<SomeClass>(api.get());

    someClass->DoWorkSafe();

    // Comment out the above line and uncomment the below line
    // to see the unsafe behavior.
    //someClass->DoWorkUnsafe();

    std::cout << "Deleting someClass\n";
    someClass.reset();

    std::cout << "Main thread sleeping for 20 seconds.\n";
    std::this_thread::sleep_for(std::chrono::seconds{ 20 });

    return 0;
}

【讨论】:

  • 但是即使对象被销毁,lambda 不会以捕获的this 指针结束吗?
  • 没错!在这种情况下,检查 "this" == nullptr 或 NULL 是不够的,因为 "this" 将悬空。我只是提到它,因为有些人甚至怀疑这种语义是否需要存在。
  • 我不认为这个答案是相关的,因为问题只涉及“假设我有一个带有方法的类;在那个方法里面”。 Lambda 只是您可以获得悬空指针的一个示例。
  • “检查这是否为空有意义吗”是问题所在。我只是提供了一个场景,它可能会随着现代 C++ 的广泛使用而变得更加普遍。
  • 这个答案非常有用,我认为它属于这个问题的主题,因为它解决了我们应该检查它是否为空的特定情况。
【解决方案4】:

FWIW,我在断言中使用了(this != NULL) 的调试检查,之前有助于捕获有缺陷的代码。并不是说如果没有崩溃代码就一定会走得太远,而是在没有内存保护的小型嵌入式系统上,断言确实有帮助。

在具有内存保护的系统上,如果使用 NULL this 指针调用操作系统通常会遇到访问冲突,因此断言 this != NULL 的价值较小。但是,请参阅 Pavel 的评论,了解为什么即使在受保护的系统上它也不一定毫无价值。

【讨论】:

  • 仍然有断言的情况,不管是否是 AV。问题是在成员函数实际尝试访问某个成员字段之前,通常不会发生 AV。他们经常只是调用其他东西(等等......),直到最终某些东西崩溃了。或者,他们可以调用另一个类的成员或全局函数,并将(假定为非空)this 作为参数传递。
  • @Pavel:说得对——我对在具有内存保护的系统上断言this 的价值的措辞有所缓和。
  • 其实,如果assert可以工作,那么主代码肯定也可以正常工作吗?
  • @Gokul:我不确定我是否完全理解您的问题,但即使断言“通过”,如果您的程序使用指针算术做坏事或可能通过“外部”类的 NULL 指针为嵌入在另一个类中的对象调用类的成员函数。我希望这句话有某种意义。
【解决方案5】:

您的方法很可能(可能因编译器而异)能够运行并且能够返回一个值。只要它不访问任何实例变量。如果它尝试这样做,它将崩溃。

正如其他人指出的那样,您不能使用此测试来查看对象是否已被删除。即使可以,它也不会起作用,因为该对象可能会在测试之后但在测试之后执行下一行之前被另一个线程删除。请改用线程同步。

如果this 为空,则您的程序中存在错误,很可能在您的程序设计中。

【讨论】:

    【解决方案6】:

    我还要补充一点,通常最好避免使用 null 或 NULL。我认为这里的标准再次发生变化,但现在 0 确实是您要检查的内容,以绝对确保您得到您想要的。

    【讨论】:

    • 检查 NULL 没有这样的事情。
    【解决方案7】:

    这只是作为第一个参数传递给函数的指针(这正是使它成为方法的原因)。只要您不是在谈论虚拟方法和/或虚拟继承,那么是的,您会发现自己正在执行一个带有空实例的实例方法。正如其他人所说,在出现问题之前,您几乎肯定不会在该执行中走得太远,但健壮的编码可能应该使用断言检查这种情况。至少,当您怀疑它可能由于某种原因发生但需要准确跟踪它发生在哪个类/调用堆栈中时,这是有道理的。

    【讨论】:

      【解决方案8】:

      我知道这是一个老问题,但我想我会分享我使用 Lambda 捕获的经验

      #include <iostream>
      #include <memory>
      
      using std::unique_ptr;
      using std::make_unique;
      using std::cout;
      using std::endl;
      
      class foo {
      public:
          foo(int no) : no_(no) {
      
          }
      
          template <typename Lambda>
          void lambda_func(Lambda&& l) {
              cout << "No is " << no_ << endl;
              l();
          }
      
      private:
          int no_;
      };
      
      int main() {
          auto f = std::make_unique<foo>(10);
      
          f->lambda_func([f = std::move(f)] () mutable {
              cout << "lambda ==> " << endl;
              cout << "lambda <== " << endl;
          });
      
          return 0;
      }
      

      此代码段错误

      $ g++ -std=c++14  uniqueptr.cpp  
      $ ./a.out 
      Segmentation fault (core dumped)
      

      如果我从lambda_func 中删除std::cout 语句,代码将运行到完成。

      看起来,这条语句f-&gt;lambda_func([f = std::move(f)] () mutable { 在调用成员函数之前处理 lambda 捕获。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2013-07-18
        • 2013-07-12
        • 2013-07-27
        • 2015-08-09
        • 2017-06-14
        • 2017-10-07
        • 2019-09-30
        相关资源
        最近更新 更多