我应该分配还是重置unique_ptr?

87

考虑到一个拥有的对象的寿命与其所有者相关联的常见情况,我可以有两种方式使用唯一指针 . .

它可以被赋值:

class owner
{
    std::unique_ptr<someObject> owned;    
public:
    owner()
    {
        owned=std::unique_ptr<someObject>(new someObject());        
    }
};

可以使用重置方法:

class owner
{
    std::unique_ptr<someObject> owned;    
public:
    owner()
    {
        owned.reset(new someObject());
    }
};

为了最佳实践,我应该偏爱哪种形式?

编辑: 抱歉,我过于简化了。堆分配发生在初始化方法中而不是构造函数中。因此,我不能使用初始化器列表。

3个回答

55

根据unique_ptr的operator=文档:

将r所指向的对象的所有权转移给*this,就像调用reset(r.release())后再从std::forward<E>(r.get_deleter())中分配一个赋值操作那样。

你只需要使用reset调用即可,因此直接调用它更简单。


59
虽然已经很晚了,但是为了日后的读者,我想指出Scott Meyers在他的书"Effective Modern C++"中提到,std::make_unique<T>更加可取,因为它提供了异常安全:owner = std::make_unique<someObject>()。问题在于std::make_unique<T>是C++14的一部分(而不是C++11),但您可以在这里找到here一个好的实现,可以复制粘贴。 - Francis Moy
@FrancisMoy 可以指出异常安全性的情况吗? - Mayur
这里不涉及到异常安全的问题。异常安全性是在向函数传递多个参数时才相关,如果每个参数都是使用make_unique创建的,那么更安全。 - undefined

19

做这件事的正确方式(你没有列出来)是使用owned的构造函数:

owner() : owned(new someObject())
{}

除此之外,我更喜欢使用reset,因为在这种情况下不会创建一个无用的中间实例(尽管在机器级别上可能没有任何区别,因为优化器可以在那里做很多工作)。


3
“我希望你重置,这样就不会产生无用的中间实例。” - 明白了,谢谢。 - learnvst

-1
简短回答:这两种解决方案是等效的。选择哪个取决于个人风格或者你想要表达的意思。
详细解释:最好且最可取的解决方案是使用不同的设计:尽量避免使用任何生命周期状态。这可以通过立即使用新分配的堆实例对unique_ptr进行初始化来实现:
class owner
{
    std::unique_ptr<someObject> owned;    
public:
    owner()
        : owned{new someObject()}
        { }
};

为什么这样更可取呢?因为它要么成功,要么失败。如果分配失败或者someObject的构造函数出错,owner的构造也会抛出异常。这意味着:你只能获得一个完整且有效的owner实例。作为额外的好处,没有中介参与其中。
然而,有些情况下你无法在初始化时创建。特别是当你的使用场景明确要求生命周期状态时:可能存在这样一种情况,你有时需要owned为空,并且只有在外部事件触发时才将someObject放入其中。
在这种情况下,问题就出现了,我们应该使用make_unique赋值还是使用reset()。从功能上来说,两者是等价的。两者都可以安全地清理另一个位于unique_ptr中的someObject实例。两者都会转移所有权并将删除器一同移动。
  • 使用reset()意味着你要对已经存在的东西进行更改重新设置。在unique_ptr之前为空的情况下,这可能会引起误导。此外,reset()的解决方案更短,更明确(你可以看到"new")。
  • 使用赋值形式的make_unique在更抽象的层面上传达了意义:"创建一个唯一的someObject实例并将其放在这里"。有些人认为这更清晰、更精确,而其他人则认为它隐藏了实际的分配。此外,make_unique创建了一个临时对象,然后将其移动到目标位置。优化器可以完全省略这一步。

异常安全性

一般来说,make_unique在某些情况下可以改变异常安全性。因此,通常建议养成使用make_unique的习惯。实际上,这在创建多个受唯一管理的对象时变得相关。

someFunction(make_unique<someObject>(arg1),
             make_unique<someObject>(arg2));

如果其中一个构造函数失败,另一个构造函数总是能够被安全地管理。
然而,在这里讨论的情况下,我们只创建一个对象。那么在出现异常的情况下会发生什么呢?
- 如果我们使用 `reset(new someObject)`,参数会首先被创建。如果这个过程出错,没关系,`reset` 函数不会被调用。但是,如果 `reset()` 需要清理一个已存在的实例,并且那个实例的析构函数抛出异常,那么我们就有了一个问题:`reset()` 调用会被异常中断,新分配的堆内存参数会泄漏。
- 另一方面,如果我们从 `make_unique()` 进行赋值,这种特殊情况会被安全处理:如果旧实例的析构函数抛出异常,栈展开将调用 `make_unique()` 返回的临时对象的析构函数。
但是,这只是一个大部分是理论的论点。在实践中,有一种“社会契约”,即正派的人们在析构过程中不会抛出异常。这是有充分理由的:在析构函数调用时抛出异常的对象会带来无尽的痛苦,基本上会危及处理任何“情况”的能力。

你不想在构造函数初始化列表中直接调用operator new。最好从make_unique的结果中进行移动构造。 - undefined
使用new进行分配并没有本质上的邪恶之处。智能指针可以防止内存泄漏。 @ben-voigt:大约一半的人喜欢抽象,另一半喜欢看到真实的东西。如果你将概念("allocation")视为真实的东西,或者将实现(operator new)视为真实的东西,这是一种态度问题。你不应该将其中一个或另一个描述为邪恶。 - undefined

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