std::shared_ptr:reset()与赋值操作的区别

68
这是一个基础问题,但我没有找到先前的帖子。以下问题的标题听起来可能与我的问题相同,但问题本身与标题不符:is it better to use shared_ptr.reset or operator =? 我对std :: shared_ptr的reset()成员函数的目的感到困惑:除了赋值运算符之外,它有什么作用?
具体而言,考虑到定义:
auto p = std::make_shared<int>(1);
  1. 以下两行代码是否等价:

    p = std::make_shared<int>(5);
    p.reset(new int(5));
    
  2. 这些怎么样:

    p = nullptr;
    p.reset();
    
    如果在两种情况下这两行是等效的,那么reset()的目的是什么?
    编辑:让我重新表述问题以更好地强调其重点。问题是:是否存在一种情况,reset()可以让我们实现一些不太容易实现的东西?

“不应该被拥有”?这是强制执行的还是只是一种协议?如果这个评论连同一个例子成为一个完整的回复,那就太好了。 - AlwaysLearning
1
这归结于使用原始指针构造shared_ptr和使用make_shared之间的区别,这应该很容易找到。提示:有一个区别,有时它可能是显著的。 - Ulrich Eckhardt
@MeirGoldenberg,这是由未定义的行为所强制执行的(如果您可以考虑到这一点)。请考虑一下,如果您在两个不同的shared_ptr上调用reset并传递该内存块,会发生什么。当这两个指针超出范围时,它们将尝试删除相同的内存块 - 因此会导致未定义的行为。 - Alejandro
3
жҲ‘еҸҜд»ҘеңЁдёӨз§Қжғ…еҶөдёӢдҪҝз”ЁеҺҹе§ӢжҢҮй’Ҳпјҡp = std::shared_ptr<int>(new int(5));е’Ңp.reset(new int(5));гҖӮзҺ°еңЁжңүд»Җд№ҲеҢәеҲ«пјҹ - AlwaysLearning
@AlwaysLearning auto p = std::make_shared<int>(1); p = nullptr; 我认为它无法编译。 - John
显示剩余5条评论
4个回答

43

使用 reset() 时,传递给 reset() 的参数不需要是托管对象(也不能是),而使用 = 时,右侧必须是托管对象。

因此,这两行代码将给您相同的结果:

p = std::make_shared<int>(5); // assign to a newly created shared pointer
p.reset(new int(5)); // take control of a newly created pointer

但是我们无法做到:

p = new int(5); // compiler error no suitable overload
p.reset(std::make_shared<int>(5).get()); // uh oh undefined behavior

如果没有reset(),你将无法重新分配共享指针到另一个原始指针而不创建一个共享指针并将其分配。如果没有=,你将无法使一个共享指针指向另一个共享指针。


2
我理解它们的不同用法。我的问题是:是否存在一种情况,reset()可以让我实现一些不容易实现的东西? - AlwaysLearning
2
通过 make_shared 创建的 shared_ptr 与通过 .reset(new T) 创建的 shared_ptr 是不同的。 - Yakk - Adam Nevraumont
1
有什么不同之处? - MaxNoe
2
使用make_shared可以将指针和共享状态分配到单个对象中,只需调用一次new。当您执行reset时,必须将其拆分为两个单独的分配。 - NathanOliver

7

reset在某些情况下可以避免动态内存分配。考虑以下代码:

std::shared_ptr<int> p{new int{}};  // 1
p.reset(new int{});                 // 2

在第一行中,有两个动态内存分配,一个是为int对象,另一个是为shared_ptr的控制块进行分配,控制块将跟踪对托管对象的强/弱引用数。
在第二行中,又会有一个新的int对象进行动态内存分配。在reset函数体内,shared_ptr将确定以前托管的int没有其他强引用,因此必须delete它。由于也没有任何弱引用,它也可以释放控制块,但在这种情况下,实现最好重用相同的控制块,因为否则它仍然需要分配一个新的控制块。
如果您总是必须使用赋值,则不可能实现上述行为。
std::shared_ptr<int> p{new int{}};    // 1
p = std::shared_ptr<int>{new int{}};  // 2

在这种情况下,第二次调用第2行的shared_ptr构造函数已经分配了一个控制块,因此p将必须释放自己现有的控制块。

我假设当您使用make_shared创建原始shared_ptr时,这不适用? - Nir Friedman
1
@NirFriedman 不太可能,但也不完全不合理。你可以想象一个库实现会根据 sizeof(ControlBlock) + sizeof(T) 做出决策,认为避免动态分配更值得。然后它可以销毁对象,但不释放控制块并获取新指针的所有权。 - Praetorian
1
一般来说,我认为使用 reset 会鼓励不良实践,应该避免使用,但我认为这是一个正确的答案。在某些情况下,使用 reset 可能是一种性能优化,但在进行更改之前,我会检查改进是否可衡量。 - Chris Drew
2
@ChrisDrew 是的,测量很重要,因为在更大的范围内,重置是否更快并不明显,因为make_shared在单个分配中创建控制块和对象,这意味着在使用时缓存局部性可能会导致make_shared胜过reset,具体取决于实际使用的shared_ptr。换句话说,仅查看修改shared_ptr的性能是不足够的,而是要考虑修改+使用的综合性能影响。(这排除了没有任何真实使用场景的小型通用测试)。 - AnorZaken
1
值得注意的是,libc++libstdc++都通过构造一个新的shared_ptr(以及一个新的控制块)并与*this交换来实现reset - Dvir Yitzchaki

2

我不会解释你第一个子问题的理由,即通过make_shared或指针构建之间的区别,因为这个区别在几个不同的地方都有强调,包括这个优秀的问题

然而,我认为区分使用resetoperator=是有益的。前者通过销毁它来放弃由shared_ptr管理的资源(如果shared_ptr恰好是唯一的所有者),或者通过递减引用计数。后者意味着与另一个shared_ptr共享所有权(除非你正在移动构造)。

正如我在评论中提到的,重要的是将传递给reset的指针不被另一个shared或unique指针所拥有,因为在两个独立的管理器被销毁时,这将导致未定义的行为——它们都将尝试删除资源。

reset的一个用例可能是共享资源的惰性初始化。只有在真正需要时,你才希望shared_ptr管理某些资源,例如内存。进行直接分配,例如:

std::shared_ptr<resource> shared_resource(new resource(/*construct a resource*/));

如果一个东西永远不会被使用,那么提前初始化可能是浪费的。如果要使用延迟初始化来解决这个问题,可以采用类似下面的方式:
std::shared_ptr<resource> shared_resource;
void use_resource()
{
       if(!shared_resource)
       {
            shared_resource.reset(new resource(...));
       }

       shared_resource->do_foo();
}

在这种情况下使用 reset 比执行 swap 或分配给临时的 shared_ptr 更加简洁。

后者(即赋值)意味着与另一个shared_ptr共享所有权。我认为你的意思是将其与reset()进行对比,后者使p成为非托管资源的所有者,这意味着独占所有权。我越读越想知道:谁会使用reset()?有没有使用此工具的示例? - AlwaysLearning
@MeirGoldenberg,更新了一个潜在的例子! - Alejandro
2
我不太确定为什么你的例子不能像使用赋值运算符一样容易地完成。 - Nir Friedman
1
我觉得shared_resource = std::make_shared<resource>(...);非常简洁。 - Chris Drew

1

reset()函数用于改变现有shared_ptr的托管对象。

p = std::shared_ptr(new int(5)); 和 p.reset(new int(5));

前者涉及创建新的shared_ptr并将其移动到变量中。后者不会创建新的对象,它只是简单地改变由shared_ptr托管的底层指针。

换句话说,这两个函数适用于不同的情况。赋值用于当您拥有一个shared_ptr时,而reset用于当您拥有一个原始指针时。

另一件需要记住的事情是,在移动分配之前,shared_ptr已经在boost中可用,并且对最新版本产生了很大影响。没有移动分配能够改变shared_ptr而不制作副本是有益的,因为它节省了额外对象的簿记工作。


shared_ptr 自 C++11 标准才存在:http://en.cppreference.com/w/cpp/memory/shared_ptr。 Boost 库有一个 C++03 的 shared_ptr - NathanOliver
@NathanOliver:我的话说得有些混乱,已经重新表述以避免那种说法,但仍然提到它可能会产生影响。(尽管在任何情况下都普遍存在一种不需要复制/移动即可进行逻辑修改的能力) - Guvante

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接