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 toT*
.Y
shall be a complete type. The expressiondelete 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 pointerp
.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 anothershared_ptr
instance (use_count() > 1
), there are no side effects.- Otherwise, if
*this
owns an objectp
and a deleterd
,d(p)
is called.- Otherwise,
*this
owns a pointerp
, anddelete 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"?)
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.
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.
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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With