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

Using smart pointers to keep track of data members that may be deleted

我有两个 类 AB。我根据 A 确定性地计算 B。对于每个 A,我想用 my_B 跟踪 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 的等价物)?

您可以从 ComputeB() return 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 实例

使用 shared_ptr 和 weak_ptr

为了让您的 B 对象在 A 中保持活动状态,只要它仍在使用中,您应该在 A 中有一个类型为 [=19= 的数据成员] 这将允许访问创建的 B 对象,只要它还活着。

来自 computeB 的 return 值将是 std::shared_ptr<B>,它可以从 std::weak_ptr<B> 成员获取或创建,如果后者持有 nullptr.


线程安全

是否创建获取现有B的决定应为thread-safe。为此,您应尝试使用 lock() 方法获取 weak_ptr 持有的实际 B,然后仅当 return 值为 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;
};

复制和赋值

上述 class 在复制和赋值中的行为是有问题的,因为默认复制构造函数和默认赋值运算符将执行 member-wise copy/assignment,这可能会导致两个不同的 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 选项的建议代码,我们还需要展示一个 class B 的示例,它能够保持 constnon-constA 的引用。代码将如下所示:

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版本,见:

  • Union of const/non-const Object Pointers
  • Is const-casting via a union undefined behaviour?

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


私人创建令牌

上面的代码允许任何人创建 B 的对象,这可能会导致意外的可能性,例如通过获取 const A& a 的构造函数创建 non-const B 对象,导致在调用 getOwner().

时可能从 const 转换为 non-const

一个好的解决方案可能是阻止 B 的创建并仅允许它来自 class A。由于创建是通过 make_sharedB 的构造函数放在 Bprivate 部分并为 A 声明 friend 来完成的没用,调用 new B 的不是 A,而是 make_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