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