gmmk
gmmk

Reputation: 89

Why doesn't std::unique_ptr allow itself to be copyable?

I know that it is to prevent two copies of unique_ptr to have a possible dangling pointer where the pointed-to object could have been already deallocated and stuff like that. But why didn't they decide to allow a kind of deep copy, where instead of just copying pointers they allocate new memory and delegate the copy to the template argument type of unique_ptr.

Upvotes: 3

Views: 1674

Answers (3)

Jeff Garrett
Jeff Garrett

Reputation: 7383

The question is a good one, and the other answers fail to mention one of the subtler reasons.

Contrary to a comment, copying is compatible with exclusive ownership. A hypothetical design could allow copying of unique_ptr<T> if T is copyable. The decision to heap allocate and the decision to allow copying are distinct concerns. I'll approach the question as why did they not use that hypothetical design?

Incomplete Types

C++ had a quirk. When defining a class template Foo<T> that should be copyable when and only when T is copyable, it cannot accept incomplete types for T. Alternatively, if one accepts incomplete types, one can lie and always advertise copyability regardless of T. But, attempting to copy may be a hard error. For example, std::vector<T> always advertises that it is copyable so that it may accept incomplete types.

(Explanation: This is because the copy constructor cannot be a template, and has no return value, so SFINAE is impossible. Instead, one can introduce base classes for Foo<T> which are chosen based on the properties of T. This implies T must be complete at the instantiation of Foo<T>. This may no longer be true with C++20 and deferred constraint checks.)

unique_ptr<T> was being designed as a replacement for an owning raw pointer T*. There are undoubtedly uses where T may be incomplete. Therefore, we can either make unique_ptr<T> not copyable always, as was done, or we can lie about it. If we lie about it, classes that contain unique_ptr<T> now also lie about their copyability by default. In such a situation, is_copy_constructible<T> would become much less useful, because a wide variety of types would lie.

Was this solvable? Surely with enough work. And I think C++20 may have. However, any solution I can imagine would be a pretty large change to the language. I think this is the most compelling reason it wasn't done.

Slicing

As one answer mentioned, slicing could be confusing if (1) the pointer was to a base class and (2) copying were implemented naively and (3) no extra state was kept than the managed pointer. There are uses where slicing doesn't apply, so one could have a genuine debate about the trade-offs of slicing versus copyability.

(3) doesn't have to be the case. When the pointer is first constructed, we could stash off a function which makes a copy of the most derived type. This would require the transition from pointer to unique_ptr happen first at the most derived type. This was the approach taken for shared_ptr's deleter. But we still can't correctly advertise our copyability so copying now becomes a runtime error. This also doubles the size.

More interestingly, (2) doesn't have to be the case. clone is total boilerplate, which means implementations could write it. However, copy constructors already mean something, so there would need to be a syntax that users could write that would allow this operation. Imagine:

virtual T* clone() = default;

But this circles back around to the first point. If the user needs to write something, unique_ptr<T> copyability needs to be conditional on properties of T which is impossible if we also want to support incomplete types.

The Future

There is a proposal P1950 for indirect_value<T> which may or may not make headway. It solves most of these issues. It constrains the copy constructor, so it can correctly advertise copyability while also supporting incomplete types. It conditionally supports copying. And, it also propagates const as if it were a value, instead of a pointer. This makes it ideal for making the decision to heap allocate, without the other baggage.

It does not address slicing, but there is a sibling proposal for polymorphic types.

Upvotes: 1

Chris
Chris

Reputation: 608

It would be confusing to implicitly make a deep copy when copying a pointer. By making it neither Copy Constructible nor Copy Assignable you are forced to make a conscious decision.

In order to call the copy constructor you have to use make_unique: Why std::make_unique calls copy constructor. It will copy the old values to new memory.

Upvotes: 1

Chris Dodd
Chris Dodd

Reputation: 126203

The main problem is that just blindly calling the template argument's copy ctor with new will slice the object if it is actually a subclass, making this a very unsafe thing to do in general.

That said, I've found it useful to extend unique_ptr:

template<class T, class D = std::default_delete<T>>
class autoclone_ptr : public std::unique_ptr<T, D> {
 public:
    autoclone_ptr() = default;
    autoclone_ptr(autoclone_ptr &&) = default;
    autoclone_ptr &operator=(autoclone_ptr &&) = default;
    autoclone_ptr(const autoclone_ptr &a) : std::unique_ptr<T,D>(a ? a->clone() : nullptr) {}
    autoclone_ptr &operator=(const autoclone_ptr &a) {
        auto *t = a.get();
        this->reset(t ? t->clone() : nullptr);
        return *this; }
};

This can only be instantiated for classes that define a clone() method to avoid slicing problems.

Upvotes: 5

Related Questions