learnvst
learnvst

Reputation: 16195

Should I assign or reset a unique_ptr?

Given the common situation where the lifespan of an owned object is linked to its owner, I can use a unique pointer one of 2 ways . .

It can be assigned:

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

The reset method can be utilised:

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

In the interests of best practice, should I prefer one form over the other?

EDIT: Sorry folks. I over simplified this. The heap allocation occurs in an initialise method and not in the ctor. Therefore, I cannot use initialiser lists.

Upvotes: 90

Views: 92196

Answers (3)

Ichthyo
Ichthyo

Reputation: 8359

Short Answer: Both solutions are equivalent. Which to choose is a matter of style or what you want to express.

In Detail: The best and most preferable solution is to use a different design: try to avoid any lifecycle state. This can be achieved by initialising the unique_ptr immediately with the newly heap allocated instance:

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

Why is this preferable? because it either succeeds or fails as a whole. If the allocation fails or if the ctor of someObject blows up, the construction of owner also throws. This implies: you can get an owner instance only in complete, valid state. As an added bonus, there is no intermediary involved.

However — there are cases when you can not create at initialisation. This is especially the case when your use case explicitly calls for lifecycle state: There may be a situation where you need the owned to be empty sometimes, and only place the someObject later into it, triggered by external events.

In such a situation the question arises if we should assign from make_unique or if we should reset(). Both are equivalent in terms of functionality. Both will safely clean-up another someObject instance which happens to sit in the unique_ptr. Both will transfer ownership and move the deleter alongside.

  • using reset() implies the meaning that you change or re-set something that is already there. It may be misleading in cases where the unique_ptr was previously empty. Moreover reset() solution has the benefit of being shorter, more explicit (you can see the "new")
  • using assignment form make_unique conveys the meaning on a more abstract level: "make an unique someObject instance and place it here". Some people find that clearer and more precise, other people think it hides the actual allocation. Moreover, make_unique creates an temporary, which is moved into the target. The optimiser is allowed to elide that altogether.

Exception Safety

Generally speaking, make_unique can change the exception safety in some situations. For this reason, it is often recommended to make a habit of using make_unique. In practice, this becomes relevant if you create several unique-managed objects.

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

If the ctor of one fails, the other one is always safely managed.

However, in the situation discussed here, we only create one object. So what happens in case of an exeption?

  • If we use reset(new someObject), the argument is created first. If this blows up, fine, the reset function is not called. BUT if reset() has to clean-up an existing other instance, and the destructor of that one throws, then we've got a problem: the reset() call will be left by exception, and the newly heap allocated argument leaks.
  • on the other hand, if we assign from make_unique(), this special situation is handled safely: if the destructor of the old instance throws, stack unwinding will call the destructor of the temporary returned by make_unique().

BUT this is a mostly theoretical argument. In practice, there is kind of a »social contract« to the end that decent people do not throw on destruction. And this stands for good reason: objects throwing on dtor call can cause endless pain and basically jeopardise the ability to handle any kind of "situation".

Upvotes: 0

Kos
Kos

Reputation: 72241

From the docs of unique_ptr's operator=:

Transfers ownership of the object pointed to by r to *this as if by calling reset(r.release()) followed by an assignment from std::forward<E>(r.get_deleter()).

And all you need of that is the reset call, so it's simpler to just call it directly

Upvotes: 59

filmor
filmor

Reputation: 32202

The proper way to do this (that you didn't list) is to use the constructor of owned:

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

Apart from that I'd prefer reset as you don't create a useless intermediate instance in that case (even though there might be no difference on the machine level as the optimizer can do a lot there).

Upvotes: 19

Related Questions