什么时候std :: weak_ptr有用?


Answers:


231

一个很好的例子是缓存。

对于最近访问的对象,您希望将它们保留在内存中,因此请牢牢指向它们。您会定期扫描缓存并确定最近未访问过哪些对象。您不需要将它们保留在内存中,因此可以摆脱强指针。

但是,如果该对象正在使用中,并且其他一些代码持有指向该对象的强大指针怎么办?如果缓存摆脱了指向对象的唯一指针,则它将再也找不到它。因此,高速缓存将微弱的指针指向它需要找到的对象(如果它们恰好留在内存中)。

这正是弱指针的作用-如果对象仍然存在,它可以让您定位它,但是如果其他对象不需要它,则不能将其保留在对象周围。


8
所以std :: wake_ptr只能指向另一个指针指向的地方,而当指向的对象被删除/不再由任何其他指针指向时,它指向nullptr呢?

27
@RM:基本上,是的。当您拥有弱指针时,可以尝试将其提升为强指针。如果该对象仍然存在(因为至少有一个指向该对象的强指针仍然存在),则该操作将成功并为您提供指向该对象的强指针。如果该对象不存在(因为所有强指针都消失了),则该操作将失败(通常您会通过丢弃弱指针来做出反应)。
大卫·史瓦兹

12
当强指针使对象保持活动状态时,weak_ptr可以查看它……而不会影响对象的生存时间。
The Vivandiere's

3
我至少使用过几次,这是另一个示例,当实现观察者时,使主体维护弱指针列表并自己进行列表清理有时会变得很方便。当删除观察者时,它可以省去一点工作,从而明确地删除观察者;更重要的是,销毁观察者时,您不必具有可用主题的信息,这通常会大大简化事情。
杰森·C

3
等一下,持有shared_ptr并在应从内存中清除它时将其从列表中删除的缓存有什么问题?任何用户都将保持shared_ptr不变,并且所有用户都将使用完后立即清除缓存的资源。
rubenvb

299

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 << *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
好的,就好像您在本地将(拥有的)指针设置为null(删除内存)一样,指向同一内存的所有其他(弱)指针也都设置为null
Pat-Laugh

std::weak_ptr::lock创建一个std::shared_ptr共享被管理对象所有权的新对象。
Sahib Yar

129

另一个答案,希望更简单。(对于其他Google员工)

假设您有TeamMember对象。

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

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

您可以使用来打破这一点weak_ptr。“所有者”通常使用shared_ptr,而“所有者”通常weak_ptr对其父使用a ,并将其临时转换为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

1
这是怎么回事?如果团队被破坏,它将破坏其成员,因此shared_ptr引用计数将为0并也被破坏?
保罗

4
@paulm团队不会破坏“其”成员。整点shared_ptr是共享所有权,因此没有人有特别的责任来释放内存,当不再使用内存时,它会自动释放。除非有循环...否则您可能有多个团队共享同一位玩家(过去的团队?)。如果团队对象“拥有”成员,则无需使用“ a” shared_ptr开头。
Offirmo 2014年

1
它不会销毁它们,但是它的shared_ptr会超出范围,减少use_count,因此此时use_count为0,因此shared_ptr会删除其指向的内容吗?
保罗

2
@paulm你是对的。但由于在此示例中,团队也是shared_ptr被其“团队成员”引用,因此何时将其销毁?您所描述的是没有循环的情况。
Offirmo 2014年

14
我想这还不错。如果一个成员可以属于许多团队,则无法使用参考。
Mazyod

22

这是@jleahy给我的一个示例:假设您有一组任务,这些任务是异步执行的,并由管理std::shared_ptr<Task>。您可能希望定期对这些任务执行某项操作,因此计时器事件可能会遍历a std::vector<std::weak_ptr<Task>>并为任务提供一些处理方法。但是,同时一项任务可能同时决定不再需要它并终止。因此,计时器可以通过从弱指针中创建一个共享指针并使用该共享指针(如果它不为null)来检查任务是否仍然有效。


4
:听起来像一个很好的例子,但是您能再详细说明一下您的例子吗?我在想,当任务完成时,应该已经将其从std :: vector <std :: weak_ptr <Task >>中删除,而无需进行定期检查。因此,不确定std :: vector <std :: weak_ptr <>>在这里是否很有帮助。
Gob00st 2012年

与队列相似的注释:假设您有对象,并且将它们排队等待一些资源,则可以在等待时删除对象。因此,如果将weak_ptrs排队,则不必费心从那里的队列中删除条目。Weak_ptrs将无效,然后在加密时被丢弃。
zzz777

1
@ zzz777:使对象无效的逻辑甚至可能不知道观察者的队列或向量的存在。因此,观察者对弱指针执行了一个单独的循环,作用于仍活着的指针,并从容器中删除了死指针...
Kerrek SB 2014年

1
@KerekSB:是的,在队列的情况下,您甚至不必进行单独的循环-然后资源可用,您将丢弃过期的weak_ptrs(如果有),直到获得有效的(如果有)。
zzz777

您还可以让线程将自己从集合中删除,但这将创建依赖关系并需要锁定。
curiousguy

16

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

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";
        }
    } );
}

这是self = shared_from_this()在Boost.Asio示例中经常看到的惯用语的变体,其中待处理的异步处理程序不会延长目标对象的寿命,但是如果删除目标对象,它仍然是安全的。


为什么这么久才找到这个答案... PS,您没有使用您的捕获this
Orwellophile

@Orwellophile固定。self = shared_from_this()处理程序在同一类中调用方法时使用惯用语时的惯用力。
Emile Cormier '18年

16

shared_ptr:保存实际对象。

weak_ptr :用于lock连接到真实所有者,否则返回NULL shared_ptr

弱点

大致来说,weak_ptr角色类似于房屋中介的角色。没有中介,要出租房屋,我们可能不得不检查城市中的随机房屋。代理商确保我们仅拜访那些仍可访问且可供出租的房屋。


14

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

13

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

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

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

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

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

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

  • 您很忙并且开会重叠:会议A和会议B
  • 您决定去会议A,而您的同事去会议B
  • 您告诉您的同事,如果在会议A结束后会议B仍在进行中,您将加入
  • 可能出现以下两种情况:
    • 会议A结束而会议B仍在进行中,所以您加入
    • 会议A结束了,会议B也结束了,所以您不能参加

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

该示例说明了弱指针是如何工作的,并且在对象需要成为外部观察者但不希望承担所有权共享责任时有用。这在两个对象需要彼此指向(又称为循环引用)的情况下特别有用。使用共享指针,两个对象都无法释放,因为它们仍然被另一个对象“强烈”指向。当指针之一是弱指针时,持有弱指针的对象仍可以在需要时访问另一个对象,前提是该对象仍然存在。


6

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

  • 它不拥有对象,因此不能阻止在其他线程中删除
  • std::shared_ptr与结合使用std::weak_ptr可防止悬空指针- std::unique_ptr与与原始指针结合使用相反
  • std::weak_ptr::lock()是原子操作(另请参阅关于weak_ptr的线程安全性

考虑一项任务,将目录(〜10.000)的所有图像同时加载到内存中(例如,作为缩略图缓存)。显然,执行此操作的最佳方法是控制线程(用于处理和管理图像)以及多个工作线程(用于加载图像)。现在,这很容易。这是一个非常简化的实现(join()省略了etc,在实际实现中必须对线程进行不同的处理,等等)

// 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_ptr结合使用std::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会检查拥有指针是否未过期。


2

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的循环引用。


打破循环引用 ”如何?
curiousguy

2

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

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


弱引用如何处理循环依赖?
curiousguy

1
@curiousguy,孩子使用对父级的弱引用,然后当没有共享的(强)引用指向父级时,可以释放父级。因此,当通过子级访问父级时,必须测试弱引用以查看父级是否仍然可用。为了避免这种额外情况,循环引用跟踪机制(标记扫描或对引用计数减量进行探查,两者均具有较差的渐近性能)可以在唯一共享给父母和孩子的共享引用时打破循环共享引用其他。
谢尔比摩尔三世

@ShelbyMooreIII“ 必须进行测试,以查看父级是否仍然可用 ”是的,您必须能够对不可用的情况做出正确的反应!真实(即强)引用不会发生这种情况。这意味着弱ref并不是替换的下降:它需要改变逻辑。
curiousguy

2
@curiousguy,您没有问过“如何weak_ptr在不改变程序逻辑的情况下处理循环依赖关系,以代替它们shared_ptr?” :-)
谢尔比·摩尔

2

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

例如:

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,这样做更有意义。


2

我认为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的精彩演讲阐释了语言功能(在本例中为智能指针)的最佳使用,以确保默认情况下无泄漏 (意思是:所有东西都通过构造就位;您几乎无法解决)。这是必须注意的。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.