15.7 std::shared_ptr 的循环依赖问题,介绍 std::weak_ptr¶
By Alex on March 21st, 2017 | last modified by Alex on January 23rd, 2020
翻译by dashjay 2020.07.18
在之前的课程中,我们看到了 std::shared_ptr
如何允许我们有多个职能指正共同拥有同样的资源,然而,在具体的情况下,这可能带来很多问题,思考如下例子,这时候两个对象中的智能指针分别指向了其他对象。
#include <iostream>
#include <memory> // for std::shared_ptr
#include <string>
class Person
{
std::string m_name;
std::shared_ptr<Person> m_partner; // initially created empty
public:
Person(const std::string &name): m_name(name)
{
std::cout << m_name << " created\n";
}
~Person()
{
std::cout << m_name << " destroyed\n";
}
friend bool partnerUp(std::shared_ptr<Person> &p1, std::shared_ptr<Person> &p2)
{
if (!p1 || !p2)
return false;
p1->m_partner = p2;
p2->m_partner = p1;
std::cout << p1->m_name << " is now partnered with " << p2->m_name << "\n";
return true;
}
};
int main()
{
auto lucy = std::make_shared<Person>("Lucy"); // create a Person named "Lucy"
auto ricky = std::make_shared<Person>("Ricky"); // create a Person named "Ricky"
partnerUp(lucy, ricky); // Make "Lucy" point to "Ricky" and vice-versa
return 0;
}
在上个面的例子中,我们使用 make_shared()
动态分配了两个 Persons
,“Lucy” 和 “Ricky”(来保证他们在main()结束前被销毁
)。紧接着,我们让他们成为搭档。这个操作设置 "Lucy" 的 std::shared_ptr
指向 “Ricky”,并且使得 “Ricky” 内的 std::shared_ptr
指向 “Lucy”。共享指针被设计用来共享的,因此这样很棒,分别设置两个 Persion 为对方的搭档。
然而执行起来,这个程序没有按照期望执行:
Lucy created
Ricky created
Lucy is now partnered with Ricky
嗯,使得。没有销毁发生,发生了什么?
在 partnerUp()
被调用后,有两个只能指针指向了 “Ricky”,两个智能指针指向了 “Lucy”。
在函数的结尾,“ricky” 的共享指针先离开作用于。当那发生时,“ricky” 检查是否有任何其他的共享指针共用拥有 “Ricky”。确实有(“lucy‘s m_partner”)。因为这,它没有释放“Ricky”(如果它释放了,那么 “Lucy” 的 m_partner 就会变成悬空指针)。在这种情况下,我们现在有一个只能指针指向 “Ricky”(“Lucy” 的 m_partner)和两个只能指针指向 “Lucy”(变量 lucy 和 “Ricky” 的 m_partner)。
紧接着 lucy 的共享指针离开作用域,并且同样的事情发生了,智能指针 lucy 检查是否有其他智能指正共同拥有 “Lucy”,确实存在(“Ricky” 的 m_partner),因此 “Lucy” 不会被释放。在这种情况下,有一个智能指针指向 “Lucy”(“Ricky” 的 m_partner),和一个智能指针指向 “Ricky”(“Lucy” 的 m_partner)。
紧接着,程序结束 ———— “Lucy” 或 “Ricky” 都没有被释放!本质上 “Lucy” 最后保证了 “Ricky” 不被销毁,而且 “Ricky” 最后保证了 “Lucy” 不被销毁。
It turns out that this can happen any time shared pointers form a circular reference. 这会发生在任何时候,只要智能指针出现循环引用。
循环依赖¶
循环依赖(也叫 cyclical reference)是一系列引用中,每一个对象引用了下一个,而最后一个对象引用了第一个,造成循环。这个引用不需要是确切的 C++ 引用 ———— 它可以是指针,唯一的ID,或者任何特殊对象。
在智能指针(shared pointers)这种情况下,循环引用就会指针引起的。
这正好发生在以上的情况下:“Lucy” 指向了 “Ricky”,然后 “Ricky” 指向了 “Lucy”。如果有三个指针,你会得到相似的结果就是 A指向B,B指向C,C指向A。在环中,智能指针的效果依然是保证下一个对象的存活 ———— 最后一个对象保证了第一个对象的存活,因此,没有对象会被释放,因为他们都任为其他对象仍然需要它!
一个 reductive 的情况¶
事实证明,循环引用的问题甚至在只有一个 std::shared_ptr
的情况下成立。 ———— 一个 std::shared_ptr
引用的对象包含了一个循环。尽管这在实际情况下很不常见,我们将会展示给你,为了进一步理解:
#include <iostream>
#include <memory> // for std::shared_ptr
class Resource
{
public:
std::shared_ptr<Resource> m_ptr; // initially created empty
Resource() { std::cout << "Resource acquired\n"; }
~Resource() { std::cout << "Resource destroyed\n"; }
};
int main()
{
auto ptr1 = std::make_shared<Resource>();
ptr1->m_ptr = ptr1; // m_ptr is now sharing the Resource that contains it
return 0;
}
在上方的例子中,当 ptr1 离开作用于时,他不会释放 Resource 因为 Rresource 的 m_ptr 正在共享 Resource。紧接着没有谁会被删除 (m_ptr
从未离开作用于,所以它没有机会被删除)。因此,这个程序输出:
Resource acquired
就是这样
std::weak_ptr 用来做什么¶
std::weak_ptr
就是用来解决上方描述的循环依赖的问题。一个 “std::waek_ptr” 是一个观察者 ———— 它可以访问和同样的对象就像 std::shared_ptr
那样,但它不被认为是一个拥有者。记住,当一个智能指针离开作用域,它仅仅思考是否有其他的 std::shared_ptr
共同持有这个对象,std::weak_ptr
不算数!
让我们使用 weak_ptr 来解决之前的 Persion 问题:
#include <iostream>
#include <memory> // for std::shared_ptr and std::weak_ptr
#include <string>
class Person
{
std::string m_name;
std::weak_ptr<Person> m_partner; // note: This is now a std::weak_ptr
public:
Person(const std::string &name): m_name(name)
{
std::cout << m_name << " created\n";
}
~Person()
{
std::cout << m_name << " destroyed\n";
}
friend bool partnerUp(std::shared_ptr<Person> &p1, std::shared_ptr<Person> &p2)
{
if (!p1 || !p2)
return false;
p1->m_partner = p2;
p2->m_partner = p1;
std::cout << p1->m_name << " is now partnered with " << p2->m_name << "\n";
return true;
}
};
int main()
{
auto lucy = std::make_shared<Person>("Lucy");
auto ricky = std::make_shared<Person>("Ricky");
partnerUp(lucy, ricky);
return 0;
}
这段代码表现正常:
Lucy created
Ricky created
Lucy is now partnered with Ricky
Ricky destroyed
Lucy destroyed
从功能上来讲,它同有问题的例子工作起来几乎一样。然而,现在当 ricky
离开作用域时,它检查发现没有其他 std::shared_ptr
指向了 “Ricky”(“Lucy” 的std::weak_ptr
不算)。因此,它会释放 “Ricky”。同样的情况发生在 lucy 上。
使用 std::weak_ptr¶
std::weak_ptr
的缺点是 std::weak_ptr
不能直接被使用(它没有 -> 操作符)。要使用 std::weak_ptr
你必须先转化它成为一个 std::shared_ptr
。要完成转化,你可以使用 lock()
成员函数,这是之前的例子经过更新:
#include <iostream>
#include <memory> // for std::shared_ptr and std::weak_ptr
#include <string>
class Person
{
std::string m_name;
std::weak_ptr<Person> m_partner; // note: This is now a std::weak_ptr
public:
Person(const std::string &name) : m_name(name)
{
std::cout << m_name << " created\n";
}
~Person()
{
std::cout << m_name << " destroyed\n";
}
friend bool partnerUp(std::shared_ptr<Person> &p1, std::shared_ptr<Person> &p2)
{
if (!p1 || !p2)
return false;
p1->m_partner = p2;
p2->m_partner = p1;
std::cout << p1->m_name << " is now partnered with " << p2->m_name << "\n";
return true;
}
const std::shared_ptr<Person> getPartner() const { return m_partner.lock(); } // use lock() to convert weak_ptr to shared_ptr
const std::string& getName() const { return m_name; }
};
int main()
{
auto lucy = std::make_shared<Person>("Lucy");
auto ricky = std::make_shared<Person>("Ricky");
partnerUp(lucy, ricky);
auto partner = ricky->getPartner(); // get shared_ptr to Ricky's partner
std::cout << ricky->getName() << "'s partner is: " << partner->getName() << '\n';
return 0;
}
输出:
Lucy created
Ricky created
Lucy is now partnered with Ricky
Ricky's partner is: Lucy
Ricky destroyed
Lucy destroyed
在使用 partner 变量的时候,我们无需关心循环引,因为他就是一个函数内的局部变量。他会最终离开作用域,然后引用数量会减少1.
结论¶
当你需要多个智能指针管理同一个资源时,可以用std::shared_ptr
。资源将会在最后一个 std::shared_ptr
离开作用域时销毁。
当你需要一个只能指针来访问另一个智能指针时,可以用std::weak_ptr
,但是不会得到资源的所有权