使用智能指针来跟踪可能被删除的数据成员

问题描述

我有两个班AB。我从B确定地计算A。对于每个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等效)?

解决方法

使用shared_ptr和weak_ptr

为了使B对象在A中保持活动状态,只要该对象仍在使用中,就应该在A类型为std::weak_ptr<B>的数据成员中使用只要创建的B对象仍然有效,就可以访问它。

computeB的返回值为std::shared_ptr<B>,可以从std::weak_ptr<B>成员那里获取,也可以创建(如果后者持有nullptr的话)。


线程安全

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


代码如下:

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持有{{ 1}}移至相同的weak_ptr。很有可能这不是您想要的,特别是如果B是可变的(即可以更改其内部值)的话。

要显示建议的复制和赋值代码,我们假设A拥有一个int成员。代码如下:

A

保持常量性

在上面的代码中,无法在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; }; 对象上进行对ComputeB()的调用。如果要支持,我们需要此函数的const版本。就语义而言,我更喜欢将此方法( const non-const 版本)重命名为const A

要显示建议的代码以添加在getB对象上调用getB的选项,我们还需要提供一个示例const A的示例,该示例可以容纳 const non-const B的引用。代码如下:

A

对于B:

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;
};

关于使用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; } }; 管理同一指针的 const 非const 版本,请参见:

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


私人创建令牌

上面的代码允许任何人创建union的对象,这可能会导致不希望的可能性,例如通过获取B的构造函数创建一个非const B对象,结果是调用const A& a时可能从const转换为非const。

一个好的解决方案可能是阻止getOwner()的创建,并仅允许其从类B开始。由于创建是通过A完成的,因此将make_shared的构造函数放在B的{​​{1}}部分中,并为private声明了B如果没有帮助,不是friend在呼叫A,而是A。因此,我们采用私有令牌方法,如以下代码所示:

new B

对于B:

make_shared

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

,

您可以从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实例