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

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

本文关键字:删除 数据成员 智能 指针 跟踪      更新时间:2024-03-29

我有两个类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的等价物)?

使用 shared_ptr 和 weak_ptr

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

来自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持有对同一Bweak_ptr。这很可能不是你想要的,特别是如果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 版本的函数。对于语义问题,我更喜欢将此方法(常量和非常量版本)重命名为getB

要提供建议的代码,添加在const A对象上调用getB的选项,我们还需要提供一个类B的示例,该示例能够保存对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管理同一指针的const和非const版本,请参阅:

常量
  • /非常量对象指针的联合
  • 通过工会进行常量铸造是未定义的行为吗?

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


私有创建令牌

上面的代码允许任何人创建可能导致不希望的可能性的B对象,例如通过获得const A& a的构造函数创建一个非常量B对象,导致在调用getOwner()时从常量转换为非常量。

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

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

你可以从 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 实例绑定