Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Does shared_ptr's dtor require the use of a "deleter"?

It's widely known that you can use a shared_ptr to store a pointer to an incomplete type, as long as the pointer can be deleted (with well-defined behaviour) during the construction of the shared_ptr. For example, the PIMPL technique:

struct interface
{
    interface();                 // out-of-line definition required
    ~interface() = default;   // public inline member, even if implicitly defined
    void foo();
private:
    struct impl;                 // incomplete type
    std::shared_ptr<impl> pimpl; // pointer to incomplete type
};

[main.cpp]

int main()
{
    interface i;
    i.foo();
}

[interface.cpp]

struct interface::impl
{
    void foo()
    {
        std::cout << "woof!\n";
    }
};

interface::interface()
    : pimpl( new impl ) // `delete impl` is well-formed at this point
{}

void interface::foo()
{
    pimpl->foo();
}

This works as an "deleter object" "owner object" (*) is created during the construction of the shared_ptr in pimpl( new impl ), and stored after type erasure inside the shared_ptr. This "owner object" is later used to destroy the object pointed to. That's why it should be safe to provide an inline destructor of interface.

Question: Where does the Standard guarantee that it's safe?

(*) Not a deleter in terms of the Standard, see below, but it does either call the custom deleter or invokes the delete-expression. This object is typically stored as part of the bookkeeping object, applying type erasure and invoking the custom deleter / delete-expression in a virtual function. At this point, the delete-expression should be well-formed as well.


Referring to the latest draft in the github repository (94c8fc71, revising N3797), [util.smartptr.shared.const]

template<class Y> explicit shared_ptr(Y* p);

3    Requires: p shall be convertible to T*. Y shall be a complete type. The expression delete p shall be well formed, shall have well defined behavior, and shall not throw exceptions.

4    Effects: Constructs a shared_ptr object that owns the pointer p.

5    Postconditions: use_count() == 1 && get() == p.

6    Throws: bad_alloc, or an implementation-defined exception when a resource other than memory could not be obtained.

Note: For this ctor, shared_ptr is not required to own a deleter. By deleter, the Standard seems to mean custom deleter, such as you provide during the construction as an additional parameter (or the shared_ptr acquires/shares one from another shared_ptr, e.g. through copy-assignment). Also see (also see [util.smartptr.shared.const]/9). The implementations (boost, libstdc++, MSVC, and I guess every sane implementation) always store an "owner object".

As a deleter is a custom deleter, the destructor of shared_ptr is defined in terms of delete (delete-expression) if there's no custom deleter:

[util.smartptr.shared.dest]

~shared_ptr();

1    Effects:

  • If *this is empty or shares ownership with another shared_ptr instance (use_count() > 1), there are no side effects.
  • Otherwise, if *this owns an object p and a deleter d, d(p) is called.
  • Otherwise, *this owns a pointer p, and delete p is called.

I'll assume the intent is that an implementation is required to correctly delete the stored pointer even if in the scope of the shared_ptr dtor, the delete-expression is ill-formed or would invoke UB. (The delete-expression must be well-formed and have well-defined behaviour in the ctor.) So, the question is

Question: Where is this required?

(Or am I just too nit-picky and it's obvious somehow that the implementations are required to use an "owner object"?)

like image 243
dyp Avatar asked Nov 07 '13 20:11

dyp


People also ask

Do I need to delete shared pointer?

So no, you shouldn't. The purpose of shared_ptr is to manage an object that no one "person" has the right or responsibility to delete, because there could be others sharing ownership.

What is deleter c++?

The deleter is part of the type of unique_ptr . And since the functor/lambda that is stateless, its type fully encodes everything there is to know about this without any size involvement. Using function pointer takes one pointer size and std::function takes even more size.

What does reset do on a shared_ ptr?

std::shared_ptr<T>::reset. Replaces the managed object with an object pointed to by ptr . Optional deleter d can be supplied, which is later used to destroy the new object when no shared_ptr objects own it. By default, delete expression is used as deleter.


1 Answers

Question: Where is this required?

If it wasn't required the destructor would have undefined behaviour, and the standard is not in the habit of requiring undefined behaviour :-)

If you meet the preconditions of the constructor, then the destructor will not invoke undefined behaviour. How the implementation ensures that is unspecified, but you can assume it gets it right, and you don't need to know how. If the implementation wasn't expected to Do The Right Thing then the destructor would have a precondition.

(Or am I just too nit-picky and it's obvious somehow that the implementations are required to use a "owner object"?)

Yes, there has to be some additional object created to own the pointer, because the reference counts (or other bookkeeping data) must be on the heap and not part of any specific shared_ptr instance, because it might need to out-live any specific instance. So yes, there is an extra object, which owns the pointer, which you can call an owner object. If no deleter is supplied by the user then that owner object just calls delete. For example:

template<typename T>
struct SpOwner {
  long count;
  long weak_count;
  T* ptr;
  virtual void dispose() { delete ptr; }
  // ...
};

template<typename T, typename Del>
struct SpOwnerWithDeleter : SpOwner<T> {
  Del del;
  virtual void dispose() { del(this->ptr); }
  // ...
};

Now a shared_ptr has a SpOwner* and when the count drops to zero it invokes the virtual function dispose() which either calls delete or invokes the deleter, depending on how the object was constructed. The decision of whether to construct an SpOwner or an SpOwnerWithDeleter is made when the shared_ptr is constructed, and that type is still the same when the shared_ptr is destroyed, so if it needs to dispose of the owned pointer then it will Do The Right Thing.

like image 159
Jonathan Wakely Avatar answered Oct 12 '22 21:10

Jonathan Wakely