有没有必要检查"this"是否为null?

65

假设我有一个类,其中有一个成员函数。在该方法内部,我检查this == nullptr,如果是,则返回错误代码。

如果this为空,则表示对象已被删除。该方法是否能够返回任何内容?

更新:我忘了提及该方法可以从多个线程调用,并且可能会导致对象在另一个线程处于成员函数内时被删除。


6
这并不意味着对象已被删除。删除指针并不会自动将其清零,而 ((Foo*)0)->foo() 是完全有效的语法。只要 foo() 不是虚函数,这甚至可以在大多数编译器上运行,但这样做很糟糕。 - Tim Sylvester
10
可能会导致在其它线程调用该方法时删除对象。这是不可接受的,因为当其他代码(无论是在同一线程还是不同线程中)仍然持有对象引用并可能在使用它时,您不能删除该对象。此外,这也不会导致其他线程中的 this 变成 null。 - Steve Jessop
我现在正在看一个情况,这明显是NULL。只有上帝知道为什么。 - Owl
每当父类为抽象类时,“this”似乎为空。 - Owl
从GCC 6.2发布说明中:值范围传播现在假定C++成员函数的this指针是非空的。 - user2672165
9个回答

81
有必要检查this==null吗?我在进行代码审阅时发现了这个问题。在标准的C++中,不需要检查,因为对空指针的任何调用都已经是未定义行为,所以任何依赖这种检查的代码都是非标准的(甚至不能保证检查将被执行)。请注意,对于非虚函数也是如此。然而,一些实现允许this==0,因此专门为这些实现编写的库有时会将其用作hack。VC++和MFC就是这样的一对好例子,我记得曾经在MFC源代码中看到过if(this==NULL)的检查,但我不记得确切的代码是什么。它也可能是作为调试辅助工具存在的,因为在过去,由于调用者的错误,这段代码曾被this==0命中,因此插入了一个检查来捕获将来的情况。但是,对于这种情况,断言更有意义。
如果this == null那么就意味着对象已被删除吗?不是的。这意味着在空指针上调用了一个方法或从空指针获取了一个引用(尽管获取这样的引用已经是未定义行为)。这与delete没有任何关系,并且不需要该类型的任何对象曾经存在过。

9
使用 delete 关键字时,通常相反的情况会发生 -- 如果你使用 delete 删除一个对象,它并不会被设置为 NULL。如果你试图调用已删除对象的方法,你会发现 this != NULL,但是如果该对象的内存已经被其他对象重新使用,那么它很可能会崩溃或表现异常。因此需要格外小心。 - Adam Rosenfield
5
虽然 delete 通常会得到一个左值,但是需要考虑 delete this + 0; 这种情况,它可能不一定会得到一个左值。请注意,这只是翻译,不包括任何解释或其他内容。 - GManNickG
9
Visual Studio 6于1998年6月发布。C ++标准于9月份出版。微软本可以预期到这个标准,但原则上,许多标准之前的编译器没有实现这个标准,这并不奇怪。;-) - Steve Jessop
4
只是为了好玩,这里有一篇关于MFC及其使用if (this == 0)的不错文章:http://www.viva64.com/en/b/0226/ - Göran Roseen
7
请注意,MFC 是一个有点特殊的情况,因为它从未真正打算与除 VC++ 以外的任何东西一起使用,并且由于团队之间保持联系,MFC 团队可以依赖这样的实现细节(并确保它们将长期存在,并且按照预期行事)。由于即使对于 VC++ 来说,这种行为也没有其他文件记录或保证,因此第三方库不能依赖它。 - Pavel Minaev
显示剩余3条评论

28

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

简单来说,你真的需要使用互斥锁或其他东西来同步访问这个变量。你需要确保this永远不为空,否则你会遇到问题。


6
“你需要确保这个东西永远不为null。”- 我认为更好的方法是确保operator->的左操作数永远不为null :) 但除此之外,我希望我能够给这个+10分。 - Pavel Minaev
他不能使用线程同步技术吗? - user13947194
1
@user13947194 是的,“线程同步技术”是说“互斥量或其他什么东西”的更恰当的说法。 - Tim Sylvester

10

我知道这已经过时了,但是我觉得现在我们正在处理C++11-17,有人应该提到lambda表达式。如果你将"this"对象捕捉到一个lambda表达式中,并且该lambda表达式将在稍后的时间点以异步方式调用,那么在该lambda表达式被调用之前,"this"对象可能会被销毁。

例如将其作为回调函数传递给一些耗费时间的函数,该函数从单独的线程或通常情况下以异步方式运行

编辑:只是为了明确,问题是“是否有意义检查这是否为空”,我仅提供一个场景,在这种场景下,它确实有意义,并且随着现代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;
}

3
即使对象被销毁了,lambda 表达式最终也会有一个悬空的非空 captured this 指针吗? - interfect
3
没错!在这种情况下,仅检查"this" == nullptr或NULL将是不够的,因为"this"会变成悬空指针。我只是提到它,因为有些人对这种语义是否需要存在持怀疑态度。 - Josh Sanders
我认为这个答案不相关,因为问题只涉及“假设我有一个带有方法的类;在该方法内部”。Lambda只是其中一个示例,您可能会遇到悬空指针。 - user2672165
1
“检查这个是否为空有意义吗?”这是问题。我只是提供了一个情景,随着现代C++的广泛使用,它可能变得更加普遍。 - Josh Sanders
2
这个答案非常有用,我认为它应该放在这个问题的讨论串中,因为它涉及到一个特定的情况,我们需要检查是否为空。 - J.beenie

6

顺便说一下,我以前在断言中使用过(this != NULL)的调试检查,有助于捕获有缺陷的代码。虽然代码不一定会崩溃,但在没有内存保护的小型嵌入式系统上,这些断言实际上是有帮助的。

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


1
断言(asserts)仍然有其存在的意义,无论是否存在AV。问题在于,AV通常不会发生,直到成员函数实际尝试访问某个成员字段。很多时候它们只是调用其他东西(等等...),直到最终某些东西在后面崩溃。或者,它们可以调用另一个类的成员或全局函数,并将(假定非空的)this作为参数传递。 - Pavel Minaev
@Pavel:确实如此——我在关于在具有内存保护的系统上断言this的价值方面措辞稍微缓和了一些。 - Michael Burr
实际上,如果断言能够正常工作,那么主要代码肯定也能正常工作,对吧? - Gokul
@Gokul:我不确定我完全理解你的问题,但即使断言“通过”,如果你的程序在指针算术上做了坏事或者可能通过一个“外部”类中嵌入的对象调用一个类的成员函数,而这个对象是通过NULL指针来组合的,你仍然可能得到一个虚假的this指针。我希望这句话有些意义。 - Michael Burr
从我的角度来看,你说的话是不合逻辑的。操作系统与您在应用程序中发送到函数的参数无关。 - user13947194

0
至少在某个编译器上,是的,这个检查是有意义的。它会按照你的期望工作,并且有可能触发它。不过,它是否有用还有待商榷,因为良好编写的代码不应该在空指针上调用成员函数,但是assert(this);的开销很小。
以下代码在MSVC 19.37上可以编译并按预期运行。即使使用了/std:c++20 /Wall /external:anglebrackets /external:W0,也不会发出警告。
#include <cstdlib>
#include <functional>
#include <iostream>

using std::cerr, std::cout;

class Foo {
public:
    void sillyContrivance() const noexcept {
        if (this) {
            cout << "hello, world!\n";
        } else {
            cerr << "Null this pointer!\n";
        }
     }
};

int main() {
    static_cast<Foo*>(nullptr)->sillyContrivance();
    const auto closure = std::bind(&Foo::sillyContrivance, static_cast<Foo*>(nullptr));
    closure();
}

程序打印了两次“Null this pointer!”。
Clang 16.0.0会警告你,this指针不能为null,将检查转换为无操作,并打印两次“hello, world!”。GCC 13.2还会警告你,在空指针上调用成员函数,并打印两次“hello, world!”。
在实际的实际应用中,一个永远不需要解引用this的成员函数应该被声明为静态的,因此,使用Clang或GCC编译的现实代码触发此错误(例如传递一个默认初始化的包含对象指针的结构体)将在现代操作系统上导致段错误。然而,在优化掉这个检查的编译器上,这个健全性检查将是无用的。

0

你的方法很可能(在编译器之间可能会有所不同)能够运行并返回一个值。只要它不访问任何实例变量。如果它尝试这样做,它将崩溃。

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

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


-2

我知道这是一个老问题,但我想分享一下我的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->lambda_func([f = std::move(f)] () mutable {在调用成员函数之前处理lambda捕获。

-2
我还要补充一点,通常最好避免使用 null 或 NULL。我认为标准在这里又再次发生了变化,但目前来说,0 才是你想要检查以确保你得到想要的东西。

检查 NULL 并不代表任何意义。 - Lightness Races in Orbit

-2

这只是传递给函数的第一个参数指针(这正是使它成为方法的原因)。只要您不涉及虚方法和/或虚继承,那么是的,您可以发现自己执行一个实例方法,但实例为空。正如其他人所说,在出现问题之前,您几乎肯定无法在执行中取得什么成果,但健壮的编码应该检查该情况,并断言。至少,当您怀疑出现某种情况时,需要跟踪确切发生在哪个类/调用堆栈中时,这是有意义的。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接