7

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"?)

Community
  • 1
  • 1
dyp
  • 38,334
  • 13
  • 112
  • 177
  • The simple answer is that the destructor call `i->~interface()` can remain as an unresolved symbol in the `main` translation unit, so the incompleteness of the template parameter of the `interface` member is barriered away. Only in the translation unit which *defines* `interface::~interface` must `impl` be complete. – Kerrek SB Nov 07 '13 at 22:10

1 Answers1

4

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.

Jonathan Wakely
  • 166,810
  • 27
  • 341
  • 521
  • 1
    `SpOwner` is a deleter object insofar as it defines the deletion strategy. It's different from the deleter passed as a parameter because it isn't a functor. – Ben Voigt Nov 07 '13 at 21:12
  • *"Then type-erased object that owns the pointer just calls delete"* That's what I meant with "deleter object". In libstdc++: `_Sp_counted_ptr` which is stored (on the heap, as a pointer) in `__shared_count` which is stored in `__shared_ptr` which `shared_ptr` derives from. – dyp Nov 07 '13 at 21:15
  • But it isn't returned by `get_deleter`, so it's not a "deleter object" in the sense used by `shared_ptr`. There's a reason I put the words in quotes. The owner object does not "store a deleter object" (as you put it in your question), it does the deleting itself. – Jonathan Wakely Nov 07 '13 at 21:16
  • I changed the terminology in my question, maybe it's clearer now? – dyp Nov 07 '13 at 21:29
  • OK, so your question is basically "is something responsible for deleting the pointer safely?" and the answer is "yes, of course" :-) But how that's done doesn't need to be specified. There **must** be _some other object_ created to store the reference counts on the heap, as they can't be in any single `shared_ptr` object, and that other object must be created by the constructor, when the dynamic type of the pointer is known, so it (or something it creates) can know how to safely delete the pointer. An implementor would have to be pretty incompetent **not** to Do The Right Thing really! – Jonathan Wakely Nov 07 '13 at 21:35
  • So yes, it is absolutely required that some kind of "owner object" is created, because where else would you put the reference counts or other bookkeeping information? – Jonathan Wakely Nov 07 '13 at 21:38
  • Actually, I'm currently wondering *how well-specified things have to be in the Standard* ;) In this example (`shared_ptr` dtor), IMO it's not *explicitly* required to be well-behaved. There's `boost::shared_ptr` which probably widely defined not only the semantics, but also the implementation of `std::shared_ptr`, and, as you say, common sense dictates it should be safe. You could store a function pointer that deletes the object; the "owner object" here really is just an example for me (question title must be short ;) – dyp Nov 07 '13 at 21:45
  • It's explicitly required that the destructor calls `delete p` where `p` is the owned pointer, which is the one passed to the ctor, **not** the one returned by `get()` (which might be a `void*` or another type that is not the same as `p`). In order to delete the original pointer **that pointer must be stored somewhere** and that "somewhere" must be created in the constructor and type-erased (so a `shared_ptr` can refer to it) and so that somewhere **must** be constructed in a context where `delete p` is valid. It would require _extra work_ from implementors to not safely delete it! – Jonathan Wakely Nov 07 '13 at 22:05