什么时候std::weak_ptr有用?

我开始研究c++ 11的智能指针,我没有看到std::weak_ptr有任何有用的用途。有人能告诉我什么时候std::weak_ptr是有用的/必要的吗?

182070 次浏览

缓存就是一个很好的例子。

对于最近访问的对象,您希望将它们保存在内存中,因此可以保留一个指向它们的强指针。定期扫描缓存,确定最近没有访问哪些对象。你不需要把它们保存在内存中,所以你去掉强指针。

但是,如果该对象正在使用,而其他一些代码持有指向它的强指针,该怎么办?如果缓存删除了指向该对象的唯一指针,就再也找不到它了。因此,缓存保留了一个弱指针,指向它需要找到的对象,如果它们碰巧留在内存中。

这正是弱指针所做的——它允许你在一个对象仍然在附近时定位它,但如果没有其他东西需要它,它就不会保留它。

下面是@jleahy给我的一个例子:假设你有一个异步执行的任务集合,并由std::shared_ptr<Task>管理。你可能想周期性地对这些任务做一些事情,所以计时器事件可以遍历std::vector<std::weak_ptr<Task>>并给任务一些事情做。然而,同时,一个任务可能同时决定不再需要它并死亡。因此,计时器可以通过从弱指针创建一个共享指针并使用该共享指针来检查任务是否仍然活跃,前提是该共享指针不为空。

另一个答案,希望更简单。(对谷歌员工)

假设你有TeamMember对象。

显然这是一个关系:Team对象将有指向其Members的指针。成员也可能有一个指向Team对象的后向指针。

然后你就有了一个依赖循环。如果你使用shared_ptr,当你放弃对对象的引用时,对象将不再被自动释放,因为它们以循环的方式相互引用。这是内存泄漏。

你可以使用weak_ptr来打破这个规则。“所有者”通常使用shared_ptr,“所有者”使用weak_ptr对其父对象,并在需要访问其父对象时将其暂时转换为shared_ptr

存储一个弱ptr:

weak_ptr<Parent> parentWeakPtr_ = parentSharedPtr; // automatic conversion to weak from shared

然后在需要的时候使用它

shared_ptr<Parent> tempParentSharedPtr = parentWeakPtr_.lock(); // on the stack, from the weak ptr
if( !tempParentSharedPtr ) {
// yes, it may fail if the parent was freed since we stored weak_ptr
} else {
// do stuff
}
// tempParentSharedPtr is released when it goes out of scope

std::weak_ptr是解决悬空指针问题的一个很好的方法。通过使用原始指针,不可能知道所引用的数据是否已被释放。相反,通过让std::shared_ptr管理数据,并将std::weak_ptr提供给数据的用户,用户可以通过调用expired()lock()来检查数据的有效性。

单独使用std::shared_ptr无法做到这一点,因为所有std::shared_ptr实例共享数据的所有权,这些数据在std::shared_ptr的所有实例被删除之前没有被删除。下面是一个如何使用lock()检查悬浮指针的示例:

#include <iostream>
#include <memory>


int main()
{
// OLD, problem with dangling pointer
// PROBLEM: ref will point to undefined data!


int* ptr = new int(10);
int* ref = ptr;
delete ptr;


// NEW
// SOLUTION: check expired() or lock() to determine if pointer is valid


// empty definition
std::shared_ptr<int> sptr;


// takes ownership of pointer
sptr.reset(new int);
*sptr = 10;


// get pointer to data without taking ownership
std::weak_ptr<int> weak1 = sptr;


// deletes managed object, acquires new pointer
sptr.reset(new int);
*sptr = 5;


// get pointer to new data without taking ownership
std::weak_ptr<int> weak2 = sptr;


// weak1 is expired!
if(auto tmp = weak1.lock())
std::cout << "weak1 value is " << *tmp << '\n';
else
std::cout << "weak1 is expired\n";
    

// weak2 points to new data (5)
if(auto tmp = weak2.lock())
std::cout << "weak2 value is " << *tmp << '\n';
else
std::cout << "weak2 is expired\n";
}

输出

weak1 is expired
weak2 value is 5

http://en.cppreference.com/w/cpp/memory/weak_ptr Std::weak_ptr是一个智能指针,它持有对Std::shared_ptr管理的对象的非所有(“弱”)引用。它必须转换为std::shared_ptr以便访问被引用的对象

Std::weak_ptr建模临时所有权:当一个对象只有在它存在时才需要访问,并且它可能在任何时候被其他人删除时,Std::weak_ptr用于跟踪该对象,并将其转换为Std::shared_ptr以承担临时所有权。如果原始的std::shared_ptr在此时被销毁,对象的生命周期将被延长,直到临时的std::shared_ptr也被销毁。

此外,std::weak_ptr用于打破std::shared_ptr的循环引用。

weak_ptr也很好地检查对象的正确删除-特别是在单元测试中。典型的用例可能是这样的:

std::weak_ptr<X> weak_x{ shared_x };
shared_x.reset();
BOOST_CHECK(weak_x.lock());
... //do something that should remove all other copies of shared_x and hence destroy x
BOOST_CHECK(!weak_x.lock());

它们在Boost中很有用。当调用异步处理程序时,不能保证目标对象仍然存在。诀窍是将weak_ptr绑定到异步处理程序对象中,使用std::bind或lambda捕获。

void MyClass::startTimer()
{
std::weak_ptr<MyClass> weak = shared_from_this();
timer_.async_wait( [weak](const boost::system::error_code& ec)
{
auto self = weak.lock();
if (self)
{
self->handleTimeout();
}
else
{
std::cout << "Target object no longer exists!\n";
}
} );
}

这是在Boost中经常看到的self = shared_from_this()习语的变体。Asio示例,其中挂起的异步处理程序将延长目标对象的生命周期,但如果目标对象被删除,则仍然是安全的。

共享指针有一个缺点: Shared_pointer不能处理父子周期依赖关系。如果父类使用父类的对象使用共享指针,则表示在同一文件中,如果子类使用父类的对象。共享指针将无法析构所有对象,甚至在循环依赖场景中共享指针根本不调用析构函数。基本上共享指针不支持引用计数机制。< / p >

我们可以使用weak_pointer来克服这个缺点。

要查看:保存实对象。

weak_ptr:使用lock连接到真正的所有者,否则返回NULL shared_ptr

weak ptr

粗略地说,weak_ptr角色类似于住房管理局的角色。如果没有中介,要想租到房子,我们可能得在城里随机找房子。中介确保我们只参观那些仍然可以访问和可用出租的房子。

在使用指针时,重要的是要了解可用的不同类型的指针,以及何时使用每种指针是有意义的。指针分为以下两类:

    <李>原始指针:
    • 原始指针[即SomeClass* ptrToSomeClass = new SomeClass();]
    • 李< / ul > < / > <李>智能指针:
      • 唯一指针[即
        std::unique_ptr<SomeClass> uniquePtrToSomeClass ( new SomeClass() );
        ]
      • 共享指针[即
        std::shared_ptr<SomeClass> sharedPtrToSomeClass ( new SomeClass() );
        ]
      • 弱指针[即
        std::weak_ptr<SomeClass> weakPtrToSomeWeakOrSharedPtr ( weakOrSharedPtr );
        ]
      • 李< / ul > < / >

      原始指针(有时被称为“遗留指针”或“C指针”)提供了“基本”指针行为,是错误和内存泄漏的常见来源。原始指针没有提供跟踪资源所有权的方法,开发人员必须手动调用“delete”以确保它们不会造成内存泄漏。如果资源是共享的,这就变得很困难,因为很难知道是否有对象仍然指向该资源。出于这些原因,通常应该避免使用原始指针,并且只在范围有限的代码的性能关键部分中使用。

      唯一指针是一个基本的智能指针,它“拥有”指向资源的底层原始指针,并负责在“拥有”唯一指针的对象超出作用域时调用delete并释放分配的内存。名称“唯一”指的是在给定的时间点上,只有一个对象可能“拥有”唯一指针。可以通过move命令将所有权转移到另一个对象,但永远不能复制或共享唯一指针。由于这些原因,在给定时间只有一个对象需要指针的情况下,唯一指针是原始指针的一个很好的替代方案,这减轻了开发人员在拥有对象的生命周期结束时释放内存的需要。

      共享指针是另一种类型的智能指针,类似于唯一指针,但允许许多对象拥有共享指针的所有权。与唯一指针一样,共享指针负责在所有对象都指向资源后释放分配的内存。它通过一种称为引用计数的技术来实现这一点。每当一个新对象获得共享指针的所有权时,引用计数加1。类似地,当对象超出作用域或停止指向资源时,引用计数减1。当引用计数达到零时,分配的内存将被释放。由于这些原因,共享指针是一种非常强大的智能指针类型,在任何需要多个对象指向同一资源的时候都应该使用它。

      最后,弱指针是另一种类型的智能指针,它们不是直接指向资源,而是指向另一个指针(弱指针或共享指针)。弱指针不能直接访问对象,但它们可以判断对象是否仍然存在或是否已经过期。弱指针可以临时转换为共享指针以访问所指向的对象(前提是它仍然存在)。为了说明这一点,考虑下面的例子:

      • 您很忙,会议A和会议B有重叠
      • 你决定去开会A,而你的同事去开会B
      • 你告诉你的同事,如果A会议结束后B会议还在进行,你也会加入
      • 可能会出现以下两种情况:
        • 会议A结束了,会议B还在进行,所以你加入了
        • 会议A已经结束,会议B也已经结束,您不能加入
        • 李< / ul > < / >

        在本例中,您有一个指向会议B的弱指针。您不是会议B的“所有者”,因此会议B可以在没有您的情况下结束,并且您不知道它是否结束,除非您检查。如果它还没有结束,你可以加入和参与,否则,你不能。这与拥有一个指向会议B的共享指针不同,因为您将同时成为会议a和会议B的“所有者”(同时参与这两个会议)。

        这个例子说明了弱指针是如何工作的,当一个对象需要是外部观察者,但不想分担所有权的责任时,弱指针是非常有用的。这在两个对象需要彼此指向的场景中特别有用(也称为循环引用)。使用共享指针,两个对象都不能被释放,因为它们仍然被另一个对象“强”指向。当其中一个指针是弱指针时,持有弱指针的对象仍然可以在需要时访问另一个对象,前提是它仍然存在。

当我们不想拥有对象时:

例:

class A
{
shared_ptr<int> sPtr1;
weak_ptr<int> wPtr1;
}

在上面的类中,wPtr1并不拥有wPtr1所指向的资源。如果资源被删除,那么wPtr1将过期。

避免循环依赖:

shard_ptr<A> <----| shared_ptr<B> <------
^             |          ^          |
|             |          |          |
|             |          |          |
|             |          |          |
|             |          |          |
class A           |     class B         |
|             |          |          |
|             ------------          |
|                                   |
-------------------------------------

现在如果我们创建类B和A的shared_ptr, both指针的use_count是2。

当shared_ptr超出作用域时,计数仍然保持1,因此A和B对象不会被删除。

class B;


class A
{
shared_ptr<B> sP1; // use weak_ptr instead to avoid CD


public:
A() {  cout << "A()" << endl; }
~A() { cout << "~A()" << endl; }


void setShared(shared_ptr<B>& p)
{
sP1 = p;
}
};


class B
{
shared_ptr<A> sP1;


public:
B() {  cout << "B()" << endl; }
~B() { cout << "~B()" << endl; }


void setShared(shared_ptr<A>& p)
{
sP1 = p;
}
};


int main()
{
shared_ptr<A> aPtr(new A);
shared_ptr<B> bPtr(new B);


aPtr->setShared(bPtr);
bPtr->setShared(aPtr);


return 0;
}

输出:

A()
B()

正如我们从输出中看到的,A和B指针永远不会被删除,从而导致内存泄漏。

为了避免这样的问题,只需在类A中使用weak_ptr而不是shared_ptr,这更有意义。

除了其他已经提到的有效用例,std::weak_ptr在多线程环境中是一个很棒的工具,因为

  • 它不拥有对象,因此不能妨碍在不同线程中删除
  • std::shared_ptrstd::weak_ptr结合使用对悬空指针是安全的——与std::unique_ptr与原始指针结合使用相反
  • std::weak_ptr::lock()是一个原子操作(另请参阅关于weak_ptr的线程安全性)

考虑一个任务,将一个目录(~10.000)的所有图像同时加载到内存中(例如作为缩略图缓存)。显然,做到这一点的最佳方法是一个控制线程(处理和管理图像)和多个工作线程(加载图像)。这是一个简单的任务。这里是一个非常简化的实现(join()等被省略,线程将不得不在一个真正的实现中被不同地处理等)

// a simplified class to hold the thumbnail and data
struct ImageData {
std::string path;
std::unique_ptr<YourFavoriteImageLibData> image;
};


// a simplified reader fn
void read( std::vector<std::shared_ptr<ImageData>> imagesToLoad ) {
for( auto& imageData : imagesToLoad )
imageData->image = YourFavoriteImageLib::load( imageData->path );
}


// a simplified manager
class Manager {
std::vector<std::shared_ptr<ImageData>> m_imageDatas;
std::vector<std::unique_ptr<std::thread>> m_threads;
public:
void load( const std::string& folderPath ) {
std::vector<std::string> imagePaths = readFolder( folderPath );
m_imageDatas = createImageDatas( imagePaths );
const unsigned numThreads = std::thread::hardware_concurrency();
std::vector<std::vector<std::shared_ptr<ImageData>>> splitDatas =
splitImageDatas( m_imageDatas, numThreads );
for( auto& dataRangeToLoad : splitDatas )
m_threads.push_back( std::make_unique<std::thread>(read, dataRangeToLoad) );
}
};

但是,如果你想中断图像的加载,例如,因为用户选择了一个不同的目录,它会变得复杂得多。或者即使你想毁掉经理。

在修改m_imageDatas字段之前,你需要线程通信并且必须停止所有加载器线程。否则,加载器将继续加载,直到所有图像都完成—即使它们已经过时。在简化的示例中,这不会太难,但在实际环境中,事情可能要复杂得多。

这些线程可能是多个管理器使用的线程池的一部分,其中一些线程正在停止,一些线程没有停止,等等。简单的参数imagesToLoad将是一个锁定的队列,这些管理器将来自不同控制线程的图像请求推入其中,读取器在另一端弹出请求(以任意顺序)。因此,交流变得困难、缓慢且容易出错。在这种情况下,避免任何额外通信的一种非常优雅的方法是将std::shared_ptrstd::weak_ptr结合使用。

// a simplified reader fn
void read( std::vector<std::weak_ptr<ImageData>> imagesToLoad ) {
for( auto& imageDataWeak : imagesToLoad ) {
std::shared_ptr<ImageData> imageData = imageDataWeak.lock();
if( !imageData )
continue;
imageData->image = YourFavoriteImageLib::load( imageData->path );
}
}


// a simplified manager
class Manager {
std::vector<std::shared_ptr<ImageData>> m_imageDatas;
std::vector<std::unique_ptr<std::thread>> m_threads;
public:
void load( const std::string& folderPath ) {
std::vector<std::string> imagePaths = readFolder( folderPath );
m_imageDatas = createImageDatas( imagePaths );
const unsigned numThreads = std::thread::hardware_concurrency();
std::vector<std::vector<std::weak_ptr<ImageData>>> splitDatas =
splitImageDatasToWeak( m_imageDatas, numThreads );
for( auto& dataRangeToLoad : splitDatas )
m_threads.push_back( std::make_unique<std::thread>(read, dataRangeToLoad) );
}
};
这个实现几乎和第一个一样简单,不需要任何额外的线程通信,并且可以在真正的实现中成为线程池/队列的一部分。由于过期的图像被跳过,而未过期的图像被处理,因此在正常操作期间线程永远不必停止。 你总是可以安全地更改路径或销毁你的管理器,因为读取器fn检查,如果拥有的指针没有过期

我看到std::weak_ptr<T>作为一个处理std::shared_ptr<T>:它允许我 来获取std::shared_ptr<T>(如果它仍然存在),但它不会扩展其 一生。在几种情况下,这种观点是有用的

// Some sort of image; very expensive to create.
std::shared_ptr< Texture > texture;


// A Widget should be able to quickly get a handle to a Texture. On the
// other hand, I don't want to keep Textures around just because a widget
// may need it.


struct Widget {
std::weak_ptr< Texture > texture_handle;
void render() {
if (auto texture = texture_handle.get(); texture) {
// do stuff with texture. Warning: `texture`
// is now extending the lifetime because it
// is a std::shared_ptr< Texture >.
} else {
// gracefully degrade; there's no texture.
}
}
};

另一个重要的场景是打破数据结构中的循环。

// Asking for trouble because a node owns the next node, and the next node owns
// the previous node: memory leak; no destructors automatically called.
struct Node {
std::shared_ptr< Node > next;
std::shared_ptr< Node > prev;
};


// Asking for trouble because a parent owns its children and children own their
// parents: memory leak; no destructors automatically called.
struct Node {
std::shared_ptr< Node > parent;
std::shared_ptr< Node > left_child;
std::shared_ptr< Node > right_child;
};


// Better: break dependencies using a std::weak_ptr (but not best way to do it;
// see Herb Sutter's talk).
struct Node {
std::shared_ptr< Node > next;
std::weak_ptr< Node > prev;
};


// Better: break dependencies using a std::weak_ptr (but not best way to do it;
// see Herb Sutter's talk).
struct Node {
std::weak_ptr< Node > parent;
std::shared_ptr< Node > left_child;
std::shared_ptr< Node > right_child;
};

Herb Sutter的演讲非常精彩解释语言的最佳使用 特性(在本例中是智能指针)来确保默认情况下无泄漏 (意思是:所有的东西都是通过建设来实现的;你很难搞砸它 )。

我看到了很多解释引用计数等的有趣答案,但我缺少一个简单的例子,演示如何使用weak_ptr防止内存泄漏。在第一个例子中,我在循环引用的类中使用shared_ptr。当类超出作用域时,它们不会被销毁。

#include<iostream>
#include<memory>
using namespace std;


class B;


class A
{
public:
shared_ptr<B>bptr;
A() {
cout << "A created" << endl;
}
~A() {
cout << "A destroyed" << endl;
}
};


class B
{
public:
shared_ptr<A>aptr;
B() {
cout << "B created" << endl;
}
~B() {
cout << "B destroyed" << endl;
}
};


int main()
{
{
shared_ptr<A> a = make_shared<A>();
shared_ptr<B> b = make_shared<B>();
a->bptr = b;
b->aptr = a;
}
// put breakpoint here
}

如果你运行代码片段,你会看到类被创建,但没有被销毁:

A created
B created

现在我们将shared_ptr's改为weak_ptr:

class B;
class A
{
public:
weak_ptr<B>bptr;


A() {
cout << "A created" << endl;
}
~A() {
cout << "A destroyed" << endl;
}
};


class B
{
public:
weak_ptr<A>aptr;


B() {
cout << "B created" << endl;
}
~B() {
cout << "B destroyed" << endl;
}
};


int main()
{
{
shared_ptr<A> a = make_shared<A>();
shared_ptr<B> b = make_shared<B>();
a->bptr = b;
b->aptr = a;
}
// put breakpoint here
}

这一次,当使用weak_ptr时,我们看到适当的类破坏:

A created
B created
B destroyed
A destroyed

受到@offirmo回复的启发,我写了这段代码,然后运行visual studio诊断工具:

#include <iostream>
#include <vector>
#include <memory>


using namespace std;


struct Member;
struct Team;


struct Member {
int x = 0;


Member(int xArg) {
x = xArg;
}


shared_ptr<Team> teamPointer;
};


struct Team {
vector<shared_ptr<Member>> members;
};


void foo() {
auto t1 = make_shared<Team>();
for (int i = 0; i < 1000000; i++) {
t1->members.push_back(make_shared<Member>(i));
t1->members.back()->teamPointer = t1;
}
}


int main() {
foo();


while (1);


return 0;
}

当指向团队的成员指针是要查看teamPointer时,在foo()完成后内存就没有空闲了,即它保持在150mb左右。

但是如果它在诊断工具中被更改为weak_ptr teamPointer,你会看到一个峰值,然后内存使用返回到大约2MB。