Does shared_ptr dtor use "deleter"?

It is widespread that you can use shared_ptr to store a pointer to an incomplete type if the pointer can be deleted (with well-defined behavior) during the construction of shared_ptr . For example, the PIMPL method:

 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 the object "delete object". The "owner object" (*) is created when building shared_ptr in pimpl( new impl ) and is saved after erasing the type inside shared_ptr . This "owner object" is later used to destroy the object that it points to. Therefore, it is safe to provide a built-in interface .

Question: Where does the standard guarantee its safety?

(*) Does not apply to the Standard, see below, but it either calls a custom divider or calls an expression-expression. This object is usually stored as part of an accounting object, applying erasure of styles and calling user delete / delete-expression in a virtual function. At this point, the delete statement should also be well-formed.


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

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

3 Required: p converted to T* . Y must be a full type. The delete p expression must be well-formed, must have well-defined behavior, and should not rule out an exception.

4 Effects: Creates a shared_ptr object to which the pointer p belongs.

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

6 Throws: bad_alloc or an exception defined by the implementation when a resource other than memory cannot be received.

Note. To do this, ctor shared_ptr does not need to own a deleter. By default, the standard means that a user debiter, for example, you provide during construction as an additional parameter (or shared_ptr acquires / shares one of the other shared_ptr , for example, by assigning a copy). Also see (See [Util.smartptr.shared.const] / 9). Implementations (boost, libstdC ++, MSVC, and I assume that every reasonable implementation) always store the "owner object".

Since the deletir is a user deleter, the shared_ptr destructor is defined in terms of delete (delete-expression) if there is no user deletion:

[util.smartptr.shared.dest]

 ~shared_ptr(); 

1 Effects:

  • If *this empty or has ownership of another instance of shared_ptr ( use_count() > 1 ), there are no side effects.
  • Otherwise, if *this belongs to p and deleter d , d(p) is called.
  • Otherwise, *this belongs to the pointer p , and delete p - is called.

I assume that it is assumed that correctly deleting the stored pointer requires an implementation, even if in the shared_ptr dtor area the expression-expression is poorly formed or will call UB. (The expression expression must be well-formed and have the correct behavior in ctor.) So, the question is:

Question: Where is this required?

(Or am I just too picky, and it is obvious that implementations should use an "owner object"?)

+6
source share
1 answer

Question: Where is this required?

If there was no need, the destructor would have undefined behavior, and the standard is not used to requiring undefined behavior :-)

If you meet the prerequisites of the constructor, the destructor will not invoke undefined behavior. How the implementation ensures that this is unspecified, but you can assume that it is correct, and you do not need to know how to do it. If the implementation did not expect "Do the right thing", then the destructor will have a precondition.

(Or am I just too picky, and it is obvious that implementations should use an "owner object"?)

Yes, there must be some additional object created to hold the pointer, because reference counting (or other accounting data) must be on the heap, and not in part of any particular shared_ptr instance, because you may need to - see any specific instance. So yes, there is an additional object to which the pointer belongs, which you can call the owner object. If no providers are submitted by the user, then this owner object simply calls delete . For instance:

 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 shared_ptr has SpOwner* , and when the counter drops to zero, it calls the virtual function dispose() , which either calls delete or calls the debiter, depending on how the object was created. The decision to build SpOwner or SpOwnerWithDeleter is made when building shared_ptr , and this type remains the same when shared_ptr destroyed, so if it needs to dispose of its pointer, it will do the right thing.

+5
source

Source: https://habr.com/ru/post/957624/


All Articles