【问题标题】:C++ - shared_ptr of abstract classC++ - 抽象类的 shared_ptr
【发布时间】:2021-06-28 10:40:54
【问题描述】:

我有以下代码:

#include <string>
#include <queue>
#include <thread>
#include <iostream>
using namespace std;

class MsgType {
public:
    virtual string getData() const = 0;
    static MsgType* getMsg();
};

class Msg1 : public MsgType {
    string getData() const override final {
        return "Msg1";
    }
};

class Msg2 : public MsgType {
    string getData() const override final {
        return "Msg2";
    }
};

queue<shared_ptr<MsgType>> allMsgs;

MsgType* MsgType::getMsg() {
    shared_ptr<MsgType> msg_sp = nullptr;
    if (!allMsgs.empty()) {
        msg_sp = allMsgs.front();
        allMsgs.pop();
    }
    if (msg_sp) {
        MsgType* mt = msg_sp.get();
        cout << "[in the method] " << mt->getData() << endl;
        return mt;
    } else {
        return nullptr;
    }
}

int main() {
    MsgType* msg1 = new Msg1();
    MsgType* msg2 = new Msg2();
    shared_ptr<MsgType> msg;
    msg.reset(msg1);
    allMsgs.push(msg);
    msg.reset(msg2);
    allMsgs.push(msg);
    MsgType* tryGetMsg = MsgType::getMsg();
    cout << "[out of the method] " << tryGetMsg->getData() << endl;
}

MsgType::getMsg() 方法中我可以看到输出,但在main() 中我看不到。我相信它正在尝试调用虚拟的MsgType::getData()
如何在此方法之外获取MsgType,以访问派生类的方法?

谢谢!

【问题讨论】:

  • 问题是您对tryGetMsg-&gt;getData() 的调用无效。由于您在getMsg 中弹出指针,并且共享ptr msg_sp 在函数出口处被销毁,因此返回的指针指向一个已经被销毁的对象。
  • 在不转移所有权时返回一个原始指针是可以的。不转移所有权时的原始指针参数是可以的。对于其他情况,请使用std::unique_ptr转到选项),或(如果必须)std::shared_ptrstd::weak_ptr

标签: c++ abstract-class shared-ptr


【解决方案1】:

直接的解决方法是从getMsg 返回一个shared_ptr:

shared_ptr<MsgType> MsgType::getMsg() {
    shared_ptr<MsgType> msg_sp;
    if (!allMsgs.empty()) {
        msg_sp = allMsgs.front();
        allMsgs.pop();
    }
    if (msg_sp) {
        cout << "[in the method] " << msg_sp->getData() << endl;
    }
    return msg_sp;
}

并停止在智能指针和原始指针之间进行不必要的转换。

消息对象必须保持活动状态,直到调用者完成使用它。由于您使用shared_ptr 来管理对象的生命周期,因此只要您想使用该对象,就需要shared_ptr 来继续存在。

一般来说,将原始指针和智能指针混合到同一个对象是有风险的,因为智能指针只能跟踪它们所知道的引用:也就是说,shared_ptr 必须知道任何地方的指针到正在共享的对象。只有当这些指针中的每一个都是shared_ptr时,它才能做到这一点。


还请注意,诊断对象生命周期问题的简单方法是编写一个记录某些内容的析构函数。这就引出了第二个问题:为了使MsgType 成为一个合适的抽象基类,它需要一个虚拟析构函数。

否则,shared_ptr 将在 refcount 变为零时尝试销毁您的对象,但无法(通常)正确地这样做。

class MsgType {
public:
    virtual ~MsgType() {}
    virtual string getData() const = 0;
};

终于转向代码审查,我故意省略了上面的getMsg

拥有一个访问全局队列的类静态方法很奇怪。如果您想保留该布局,allMsgs 队列也应该是类静态的。

相反,最好将 msg_queue 对象保留在您实际需要的任何位置,而不使用静态或全局变量。

【讨论】:

    【解决方案2】:

    这里:

    MsgType* MsgType::getMsg() {
        shared_ptr<MsgType> msg_sp = nullptr;
        if (allMsgs.empty()) {
            msg_sp = allMsgs.front();
            allMsgs.pop();
        }
        if (msg_sp) {
            MsgType* mt = msg_sp.get();
            cout << "[in the method] " << mt->getData() << endl;
            return mt;
        } else {
            return nullptr;
        }
    }
    

    allMsgs 不为空时,您复制front,然后复制pop。那时有一个shared_ptr 管理该对象:msg_sp。然后你通过get 检索一个原始指针并返回它,但是当函数返回使用计数递减到0 并且托管对象被销毁时。返回的指针无效。

    我发现您将 raw 和 shared_ptr 混合在一起有点令人困惑。当你有一个 shared_ptr 管理对象的生命周期时,你不能先得到一个原始指针,然后让 shared_ptr 销毁托管对象并仍然使用原始指针。当您不希望共享指针破坏托管对象时,您需要正确转移所有权。

    【讨论】:

    • 我明白你在说什么,但不明白如何实现。换句话说,这样做的正确方法是什么?
    • @עתודהאקדמאית 首先,停止使用原始指针。从队列中弹出 shared_ptr 意味着它的生命周期现在完全掌握在本地 msg_sp 手中。当它在函数返回时被销毁时,不再有对象;最后一个人把它毁了,你给它留下的只是一个悬空指针。
    • @עתודהאקדמאית 错误的代码并不能很好地解释你真正想要做什么。也许其他人会理解,但恕我直言,如果你解释一下代码的实际用途会更好。
    【解决方案3】:

    我不知道您为什么要以这种方式混合 std::shared_ptr 和 C 风格的指针,但让我们忽略这一点,假设这只是一个练习。

    看看你的代码的下半部分(略微减少),我们有这个:

    std::queue<std::shared_ptr<MsgType>> allMsgs;
    
    MsgType* MsgType::getMsg();
    
    int main() {
        MsgType* msg1 = new Msg1();
        std::shared_ptr<MsgType> msg;
        msg.reset(msg1);               // <--- 1. here, msg1 is owned by msg
        allMsgs.push(msg);             // <--- 2. now, msg1 is also owned by allMsgs
        msg.reset();                   // <--- 3. msg1 only owned by allMsgs
    
        MsgType* tryGetMsg = MsgType::getMsg();  // <--- see below : nobody keeping msg1 alive!
        std::cout << "[out of the method] " << tryGetMsg->getData() << std::endl;
    }
    
    MsgType* MsgType::getMsg() {
        std::shared_ptr<MsgType> msg_sp = nullptr;
        if (!allMsgs.empty()) {
            msg_sp = allMsgs.front();  // <--- 4. msg1 owned by msg_sp & allMsgs
            allMsgs.pop();             // <--- 5. msg1 owned by msg_sp only
        }
        if (msg_sp) {
            MsgType* mt = msg_sp.get();
            std::cout << "[in the method] " << mt->getData() << std::endl;
            return mt;
        } else {
            return nullptr;
        }
    }                                  // <--- 6. msg_sp destroyed... oh oh... msg1 dead :) 
    

    作为一个小补充,您可以直接从派生的基类指针构造一个共享基类指针,例如

    auto msg_sp = std::shared_ptr<MsgType>(std::make_shared<Msg1>());
    

    【讨论】:

    • 这并不能解决返回悬空指针的问题
    • @hegel5000:确实如此。我没有更改代码,只是添加了对正在发生的事情的解释。我试图解释为什么打印不起作用。 OP 声称“我相信它正在尝试调用虚拟的 MsgType::getData()。”这看起来像是一个了解正在发生的事情的练习......为什么还要返回原始 ptr?
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2012-12-04
    • 2016-04-12
    • 2019-09-05
    • 1970-01-01
    • 2020-08-12
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多