什么时候std :: weak_ptr有用吗?

时间:2012-08-19 23:00:05

标签: c++ c++11 shared-ptr smart-pointers weak-ptr

我开始研究C ++ 11的智能指针,我没有看到std::weak_ptr的任何有用用法。当std::weak_ptr有用/必要时,有人可以告诉我吗?

14 个答案:

答案 0 :(得分:256)

std::weak_ptr是解决dangling pointer问题的一种非常好的方法。通过使用原始指针,无法知道引用的数据是否已被释放。相反,通过让std::shared_ptr管理数据并向数据用户提供std::weak_ptr,用户可以通过调用expired()lock()来检查数据的有效性。< / p>

单独使用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 << *tmp << '\n';
    else
        std::cout << "weak1 is expired\n";

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

答案 1 :(得分:184)

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

对于最近访问过的对象,您希望将它们保留在内存中,因此您需要一个指向它们的强指针。您可以定期扫描缓存并确定最近未访问的对象。你不需要把它们留在内存中,所以你摆脱了强大的指针。

但是如果该对象正在使用中并且其他一些代码拥有强大的指针呢?如果缓存摆脱了它对象的唯一指针,它就永远无法再找到它。因此,如果缓存碰巧留在内存中,缓存会保留指向它需要查找的对象的弱指针。

这正是弱指针的作用 - 它允许你定位一个物体,如果它仍然存在,但如果没有别的需要它就不会保留它。

答案 2 :(得分:103)

另一个答案,希望更简单。 (对于谷歌同行)

假设您有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 failed if parent was freed since we stored weak_ptr
} else {
  // do stuff
}
// tempParentSharedPtr is released when it goes out of scope

答案 3 :(得分:18)

这是@jleahy给我的一个例子:假设你有一组任务,异步执行,并由std::shared_ptr<Task>管理。您可能希望定期对这些任务执行某些操作,因此计时器事件可以遍历std::vector<std::weak_ptr<Task>>并为任务提供一些操作。然而,同时任务可能同时决定不再需要它并且死亡。因此,计时器可以通过从弱指针创建共享指针并使用该共享指针来检查任务是否仍然存活,前提是它不为空。

答案 4 :(得分:14)

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

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.Asio示例中常见的self = shared_from_this()习语的变体,其中挂起的异步处理程序将延长目标对象的生命周期,但如果仍然安全目标对象被删除。

答案 5 :(得分:13)

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());

答案 6 :(得分:11)

shared_ptr :保留真实对象。

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

weak ptr

粗略地说,weak_ptr角色类似于住房代理商的角色。没有经纪人,要租房子,我们可能要检查城市里的随意房屋。代理商确保我们只访问那些仍可访问且可用出租的房屋。

答案 7 :(得分:8)

使用指针时,了解可用的不同指针类型以及何时使用每个指针都很重要。两种类别的指针有四种类型如下:

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

原始指针(有时称为&#34;遗留指针&#34;或者&#34; C指针&#34;)提供了&#39;裸骨&#39;指针行为,是bug和内存泄漏的常见来源。原始指针无法跟踪资源的所有权,开发人员必须调用“删除”。手动确保它们不会造成内存泄漏。如果共享资源,则这变得困难,因为知道任何对象是否仍然指向资源可能是具有挑战性的。由于这些原因,通常应该避免使用原始指针,并且只在范围有限的代码的性能关键部分中使用。

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

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

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

  • 您很忙,会议重叠:会议A和会议B
  • 您决定参加A会议,您的同事参加会议B
  • 您告诉您的同事,如果会议B在会议A结束后仍在继续,您将加入
  • 可能会出现以下两种情况:
    • 会议A结束,会议B仍在继续,所以你加入
    • 会议A结束,会议B也结束了,所以你没有加入

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

该示例说明了弱指针如何工作,并且当对象需要是外部观察者但不想要所有权的责任时非常有用。这在两个对象需要彼此指向的情况下特别有用(例如,循环引用)。使用共享指针时,两个对象都不能被释放,因为它们仍然是强烈的&#39;另一个对象指出。使用弱指针,可以在需要时访问对象,并在不再需要存在时释放它们。

答案 8 :(得分:2)

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

  • 它不拥有对象,因此不能阻止在其他线程中删除
  • std::shared_ptrstd::weak_ptr结合使用可防止悬空指针-与std::unique_ptr与原始指针结合使用相反
  • std::weak_ptr::lock()是原子操作(另请参见About thread-safety of 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会检查拥有指针是否未过期。

答案 9 :(得分:1)

http://en.cppreference.com/w/cpp/memory/weak_ptr std :: weak_ptr是一个智能指针,它包含对由std :: shared_ptr管理的对象的非拥有(&#34;弱&#34;)引用。必须将其转换为std :: shared_ptr才能访问引用的对象。

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

另外,std :: weak_ptr用于破坏std :: shared_ptr的循环引用。

答案 10 :(得分:1)

共享指针有一个缺点: shared_pointer无法处理父子循环依赖项。表示父类是否使用共享指针在子类中使用子类的对象,如果子类使用父类的对象,则在同一文件中。共享指针将无法破坏所有对象,即使共享指针在循环依赖性方案中根本不调用析构函数。基本上共享指针不支持引用计数机制。

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

答案 11 :(得分:1)

当我们不想拥有该对象时:

例如:

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,则两个指针的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即可。

答案 12 :(得分:1)

我将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 has an excellent talk解释了语言的最佳用法 功能(在本例中为智能指针)来确保默认情况下的泄漏自由 (意思是:一切都是通过构造卡入的;您几乎无法拧紧它 向上)。这是必须注意的。

答案 13 :(得分:0)

我看到很多有趣的答案,它们解释了引用计数等,但是我缺少一个简单的示例,该示例演示了如何使用intrinsicContentSize防止内存泄漏。在第一个示例中,我在循环引用的类中使用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