【问题标题】:Using smart pointers to keep track of data members that may be deleted使用智能指针跟踪可能被删除的数据成员
【发布时间】:2020-09-17 07:10:20
【问题描述】:

我有两个课程AB。我从A 确定性地计算B。对于每个A,我想跟踪Bmy_B,只要它存在。一旦 B 被破坏,我希望将 my_B 更改为 nullptr 之类的东西。

class A{
// stuff
public:
    B ComputeB(){
        if (my_B is null){
            B result = B(A);
            my_B = B; // some kind of reference
            return B(A);
        } 
        else {
            return my_B;
        }
    }
    
    ~A(){  /* Do I need a destructor? */ }
private:
    WhatTypeHere my_B;
}

B 被破坏时,什么会导致my_B 引用nullptr(或WhatTypeHere 的等效项)?

【问题讨论】:

  • 您对智能指针做了哪些研究,其中哪些特定部分您不清楚?
  • @t.niese 最后一行是我的问题。如果有人可以用智能指针回答这个问题,那么我的问题就得到了回答。虽然我也试图让这个问题对非智能指针解决方案保持开放。 “当 B 被破坏时,什么会导致 my_B 引用 nullptr(或 WhatTypeHere 的等效项)?”

标签: c++ smart-pointers datamember


【解决方案1】:

使用 shared_ptr 和 weak_ptr

为了让您的B 对象在A 中一直处于使用状态,您应该在A 中有一个std::weak_ptr<B> 类型的数据成员,这将允许访问创建的@987654329 @对象,只要它还活着。

computeB 的返回值将是 std::shared_ptr<B>,如果后者持有 nullptr,则可以从 std::weak_ptr<B> 成员获取或创建。


线程安全

创建获取现有B的决定应该是线程安全的。为此,您应尝试使用lock() 方法获取weak_ptr 持有的实际B,然后仅当返回值为nullptr 时创建一个新的。


代码如下所示:

class A {
    // stuff
public:
    std::shared_ptr<B> ComputeB() {
        std::shared_ptr<B> shared_b = my_B.lock();
        if (!shared_b){
            shared_b = std::make_shared<B>(*this);
            my_B = shared_b;
        } 
        return shared_b;
    }
    // no need for a destructor, unless "stuff" needs one
    // ~A(){} 
private:
    std::weak_ptr<B> my_B;
};

复制和分配

上述类在复制和赋值中的行为是有问题的,因为默认的复制构造函数和默认的赋值运算符会执行成员方式的复制/赋值,这可能导致两个不同的A 持有一个weak_ptr相同的B。很可能这不是您想要的,尤其是如果 A 是可变的(即可以更改其内部值)。

为了呈现复制和赋值的建议代码,我们假设A 拥有一个 int 成员。代码将如下所示:

class A {
    int i;
public:
    A(int i1): i(i1) {}
    void set(int i1) { i = i1; }
    std::shared_ptr<B> ComputeB() {
        std::shared_ptr<B> shared_b = my_B.lock();
        if (!shared_b){
            shared_b = std::make_shared<B>(*this);
            my_B = shared_b;
        } 
        return shared_b;
    }
    A(const A& a): i(a.i) {}
    A& operator=(const A& a) { i = a.i; return *this; }
    ~A() {}
private:
    std::weak_ptr<B> my_B;
};

保持常量

在上面的代码中,不能在 const A 对象上调用 ComputeB()。如果我们想支持我们需要这个函数的 const 版本。就语义而言,我更喜欢将此方法(constnon-const 版本)重命名为 getB

为了展示建议的代码,添加在const A 对象上调用getB 的选项,我们还需要展示一个类B 的示例,它能够保存一个 const非常量A的引用。代码将如下所示:

class A {
    int i;
    // to prevent code duplication for the const and non-const versions
    template<typename AType>
    static auto getB(AType&& a) {
        std::shared_ptr<B> shared_b = a.my_B.lock();
        if (!shared_b){
            shared_b = std::make_shared<B>(std::forward<AType>(a));
            a.my_B = shared_b;
        } 
        return shared_b;
    }
public:
    A(int i1): i(i1) {}
    void set(int i1) { i = i1; }
    std::shared_ptr<B> getB() {
        return getB(*this);
    }
    std::shared_ptr<const B> getB() const {
        return getB(*this);
    }
    A(const A& a): i(a.i) {}
    A& operator=(const A& a) { i = a.i; return *this; }
    ~A() {}
private:
    mutable std::weak_ptr<B> my_B;
};

对于 B:

class B {
    union Owner {
        A* const ptr;
        const A* const const_ptr;
        Owner(A& a): ptr(&a) {}
        Owner(const A& a): const_ptr(&a) {}
    } owner;
public:
    B(A& a): owner(a) {}
    B(const A& a): owner(a) {}
    const A& getOwner() const {
        return *owner.const_ptr;
    }
    A& getOwner() {
        return *owner.ptr;
    }
};

关于使用union管理同一指针的constnon-const版本,见:

工作示例:http://coliru.stacked-crooked.com/a/f696dfcf85890977


私有创建令牌

上面的代码允许任何人创建 B 的对象,这可能会导致不希望的可能性,例如通过获取 const A&amp; a 的构造函数创建非 const B 对象,从而可能会从 const 转换为 non -const 调用getOwner()时。

一个好的解决方案可能是阻止B 的创建并仅允许来自A 类。由于创建是通过make_sharedB 的构造函数放在Bprivate 部分中完成的,而Afriend 声明将无济于事,因此调用的不是A new Bmake_shared。因此,我们采用私有令牌方法,如下代码所示:

class A {    
    int i;
    // only authorized entities can create B
    class B_PrivateCreationToken {};    
    friend class B;

    template<typename AType>
    static auto getB(AType&& a) {
        std::shared_ptr<B> shared_b = a.my_B.lock();
        if (!shared_b){
            shared_b = std::make_shared<B> (
                  std::forward<AType>(a),
                  B_PrivateCreationToken{} );
            a.my_B = shared_b;
        } 
        return shared_b;
    }
public:

    // public part as in above version...

private:
    mutable std::weak_ptr<B> my_B;
};

对于 B:

class B {
    union Owner {
        A* const ptr;
        const A* const const_ptr;
        Owner(A& a): ptr(&a) {}
        Owner(const A& a): const_ptr(&a) {}
    } owner;
public:
    B(A& a, A::B_PrivateCreationToken): owner(a) {}
    B(const A& a, A::B_PrivateCreationToken): owner(a) {}

    // getOwner methods as in above version...

};

代码:http://coliru.stacked-crooked.com/a/f656a3992d666e1e

【讨论】:

    【解决方案2】:

    您可以从 ComputeB() 返回 std::shared_ptr,并使 my_B 成为 std::weak_ptr。像这样的:

    std::shared_ptr<B> ComputeB() {
        if (my_B.expired()) {
            auto result = std::make_shared<B>(*this);
            my_B = result;
            return result;
        } else {
            return std::shared_ptr<B>(my_B);
        }
    }
    
    private:
    std::weak_ptr<B> my_B;
    

    这个想法是 ComputeB 的任何调用者成为 B 实例的部分所有者,这意味着它只会在其所有 shared_ptrs 被销毁时才会被销毁。 weak_ptr 的目的是指向 B 实例而不拥有它,因此生命周期根本与 A 实例无关

    【讨论】:

    • 这是有道理的。 weak_ptr 的工作原理与它的工作原理对我来说很奇怪。我想如果ComputeB 返回一个B(要存储在堆栈上),那么当B 在离开范围时被删除时,weak_ptr 会过期吗?
    • 不,weak_ptr 专门用于 shared_ptrs,因为它能够使用 shared_ptr 的内部引用计数来查看对象是否被销毁
    • 在我看来你不能在这里返回一个常规的 B 实例,因为如果你这样做了,你每次调用函数时都必须创建一个新实例,你最终会覆盖 my_B每次都有一个指向最新实例的指针
    • 请注意,使用expired 会导致代码不是线程安全的。 my_B 可能会在通过检查后立即过期 - 因此 ComputeB 将返回 nullptr。如果是null create,最好使用my_B.lock(),否则返回。
    • @AmirKirsh "如果 my_B.lock() 创建为空,则使用它会更好,否则返回它。"如果您能澄清一下,您在这里的措辞有点混乱。
    猜你喜欢
    • 2019-12-05
    • 2013-03-16
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-09-10
    • 1970-01-01
    • 2016-08-25
    • 1970-01-01
    相关资源
    最近更新 更多