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

Does it ever make sense to check if "this" is null?

提问人:user156144 提问时间:12/4/2009 最后编辑:Jan Schultkeuser156144 更新时间:9/19/2023 访问量:45700

问:

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

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

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

C++ 指针 null

评论

6赞 Tim Sylvester 12/4/2009
这并不意味着该对象已被删除。删除指针不会自动将其清零,这是完全有效的语法。只要不是虚函数,这甚至可以在大多数编译器上运行,但它只是令人讨厌。((Foo*)0)->foo()foo()
10赞 Steve Jessop 12/4/2009
“当另一个线程位于方法内部时,可能会导致对象被删除”。这是不可接受的,当其他代码(在同一线程或不同线程中)保留它将使用的引用时,您不能删除对象。它也不会导致在另一个线程中变为 null。this
0赞 Owl 7/13/2016
我现在正在研究一种情况,这绝对是空的。只有上帝知道为什么。
0赞 Owl 7/13/2016
每当父类是抽象的时,“this”似乎都是 null。
0赞 user2672165 8/26/2016
来自 GCC 6.2 发行说明:值范围传播现在假定 C++ 成员函数的 this 指针为非 null。

答:

81赞 Pavel Minaev 12/4/2009 #1

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

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

请注意,这也适用于非虚拟功能。

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

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

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

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

评论

9赞 Adam Rosenfield 12/4/2009
对于,通常情况正好相反——如果你一个对象,它没有被设置为 NULL,然后你尝试对它调用一个方法,你会发现,但如果它的内存已经被回收供其他对象使用,它可能会崩溃或行为异常。deletedeletethis != NULL
5赞 GManNickG 12/4/2009
delete不过,可能并不总是得到 L 值,请考虑一下delete this + 0;
9赞 Steve Jessop 12/4/2009
Visual Studio 6 于 1998 年 6 月发布。C++ 标准于 9 月发布。好的,所以 Microsoft 可以预见到该标准,但原则上,许多预标准编译器不实现该标准也就不足为奇了;-)
4赞 Göran Roseen 1/4/2016
只是为了好玩,这里有一篇关于 MFC 的好文章,它使用了 if (this == 0) viva64.com/en/b/0226
7赞 Pavel Minaev 1/4/2016
请注意,MFC 是一个特例,因为它从未真正打算与 VC++ 以外的任何内容一起使用,并且由于团队保持联系,MFC 团队可以依赖这样的实现细节(并确保它们将根据需要存在,并完全按照预期运行)。由于即使对于 VC++,也没有以其他方式记录或保证此行为,因此第三方库不能依赖它。
28赞 Tim Sylvester 12/4/2009 #2

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

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

评论

6赞 Pavel Minaev 12/4/2009
“你需要确保这永远不会为空”——我认为更好的方法是确保 的左操作数永远不会为空:)但除此之外,我希望我能+10这个。operator->
0赞 user13947194 12/31/2022
难道他不能使用线程同步技术吗?
1赞 Tim Sylvester 1/1/2023
@user13947194 是的,“线程同步技术”是“互斥锁”之类的更恰当的说法。
6赞 Michael Burr 12/4/2009 #3

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

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

评论

1赞 Pavel Minaev 12/4/2009
断言仍然有理由,无论是否 AV。问题在于,在成员函数实际尝试访问某个成员字段之前,AV 通常不会发生。很多时候,他们只是调用其他东西(等等),直到最终某些东西崩溃。或者,它们可以调用另一个类或全局函数的成员,并将(假定为非 null)作为参数传递。this
0赞 Michael Burr 12/4/2009
@Pavel:没错 - 我软化了关于在具有内存保护的系统上断言的价值的措辞。this
0赞 Gokul 3/9/2010
实际上,如果 Assert 要工作,那么主代码肯定也可以工作,对吗?
0赞 Michael Burr 3/9/2010
@Gokul:我不确定我是否完全理解你的问题,但即使断言“通过”,如果你的程序在指针算术上做坏事,或者可能通过“外部”类的 NULL 指针调用一个类的成员函数,你也可能得到一个虚假的指针。我希望这句话有某种意义。this
0赞 user13947194 12/31/2022
在我看来,你说了一些不合逻辑的话。OS 与发送到应用程序中的函数的参数无关。
-2赞 Charles Eli Cheese 12/4/2009 #4

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

评论

0赞 Lightness Races in Orbit 4/14/2014
检查 NULL 不会做这样的事情。
0赞 Carsten 12/4/2009 #5

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

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

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

-2赞 user1024732 2/24/2013 #6

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

10赞 Josh Sanders 2/25/2016 #7

我知道这已经很老了,但我觉得现在我们正在处理 C++11-17,有人应该提到 lambdas。如果您将其捕获到稍后将异步调用的 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;
}

评论

3赞 interfect 3/23/2016
但是,即使对象被销毁,lambda 最终不会得到一个悬空的非 null 捕获指针吗?this
3赞 Josh Sanders 3/24/2016
正是!在这种情况下,检查 “this” == nullptr 或 NULL 是否足够,因为 “this” 将悬空。我只是提到它,因为有些人怀疑这种语义甚至需要存在。
0赞 user2672165 8/26/2016
我认为这个答案无关紧要,因为这个问题只涉及“假设我有一个有方法的类;在这种方法中”。Lambda 只是您可以获取悬空指针的一个示例。
1赞 Josh Sanders 8/26/2016
“检查这是否为空有意义”是问题所在。我只是提供了一个场景,随着现代 C++ 的广泛使用,它可能会变得更加普遍。
2赞 J.beenie 5/28/2021
这个答案非常有用,我认为它属于这个问题的线程,因为它解决了我们应该检查这是否为空的特定情况。
-2赞 Prasad Joshi 6/4/2017 #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)

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

似乎,此语句在调用成员函数之前处理 lambda 捕获。f->lambda_func([f = std::move(f)] () mutable {

0赞 Davislor 9/19/2023 #9

是的,在至少一个编译器上,检查是有意义的。它将按您的预期工作,并且可以触发它。它是否有用是值得怀疑的,因为格式良好的代码不应该在 null 指针上调用成员函数,但很便宜。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 会警告您不能为 null,将检查变成空操作,并打印两次。GCC 13.2 还会警告您正在 null 指针上调用成员函数,并且还会打印两次。thishello, world!hello, world!

在现实世界的实际使用中,一个永远不需要取消引用的成员函数将被声明,因此使用 Clang 或 GCC 编译的触发此错误的实际代码(例如传递包含对象指针的默认初始化)将在现代操作系统上段错误。但是,健全性检查对优化它的编译器毫无用处。thisstaticstruct