| Doc. no.: | P3139R1 |
| Date: | 2024-12-27 |
| Audience: | LEWG |
| Reply-to: | Zhihao Yuan <zhihao.yuan@broadcom.com> Jordan Saxonberg <jordan.saxonberg@broadcom.com> |
Pointer cast for unique_ptr
Changes
- Since R0
-
- Allow destroying delete as an alternative to virtual destructor
Introduction
We propose unique_ptr overloads for std::const_pointer_cast and std::dynamic_pointer_cast. For each kind of cast, we allow users to choose between either using the defaulted deleter or preserving the original deleter type for each kind of cast.
The table following illustrates the simpler case of the two where users expect defaulted deleters in the resulting types.
|
Given an API:
auto GetClient() -> std::unique_ptr<const Client>;
|
|
C++23
|
⚠ Owning raw pointer
std::unique_ptr<Client> client;
client.reset(const_cast<Client*>(GetClient().release()));
❌ Leak resources if dynamic_cast fails
void UseV2Client(std::unique_ptr<Client>&& client)
{
std::unique_ptr<ClientV2> v2;
v2.reset(dynamic_cast<ClientV2*>(client.release()));
|
|
P3139
|
✔
std::unique_ptr<Client> client;
client = const_pointer_cast<Client>(GetClient());
✔
void UseV2Client(std::unique_ptr<Client>&& client)
{
std::unique_ptr<ClientV2> v2;
v2 = dynamic_pointer_cast<ClientV2>(std::move(client));
|
Prior Art
Boost.SmartPtr ships all four casts (dynamic_pointer_cast, static_pointer_cast, const_pointer_cast, and reinterpret_pointer_cast) that create std::unique_ptr<T> by releasing std::unique_ptr<U> since 2016.
Motivation
- Improve resource safety
- The need for pointer casts between
unique_ptrs was spotted in code reviews from independent parties. Without coincidence, the conclusions were to use .release(), a resource-unsafe API, as a one-off solution. Such a practice encourages the use of unsafe constructs and potentially breaks future code as a result. The standard C++ should encourage the opposite.
- Express the intent of pointer cast via established vocabularies
- When people look for pointer casts between smart pointers, they look for
std::dynamic_pointer_cast, std::const_pointer_cast, etc. These names resemble dynamic_cast and const_cast and work for std::shared_ptr already. There is no simpler way to express the same intent and no reason to find a different set of names.
- Standardize existing practices
- The cpplang Slack workspace rediscovers
dynamic_pointer_cast(std::unique_ptr<T>&&) on a yearly basis, albeit boost::dynamic_pointer_cast(std::unique_ptr<T>&&) existed before the group's birth. Some of the work supports preserving the incoming deleter type. It's time to consider adopting the working parts from Boost and explore the recurring extension.
Design
unique_ptr differs from shared_ptr in a few significant ways. Obviously, we can only cast from an rvalue of unique_ptr by moving its ownership. The other differences that made impacts on the design are:
- A
shared_ptr<T> carries a type-erased deleter, while unique_ptr<T, D>'s deleter is a part of the type. The seemly intuitive unique_ptr<T> to unique_ptr<U> actually requires replacing std::default_delete<T> with std::default_delete<U>, which may not apply to the customized deleters.
- A casted
shared_ptr<U> is only an alias to the original shared_ptr<T>. The newly created object requires no deleter, and you can expect the original deleter to be able to delete the uncasted pointer. Meanwhile, unique_ptr<U, D>, in general, must deal with the question "whether D can delete the casted pointer."
- A
shared_ptr<T> owns a "real" pointer, while unique_ptr<T, D> can customize its pointer type as indicated by its pointer typedef. Converting between the pointer types may create loopholes as the unique_ptr<T[], D> specializations reused this mechanism.
It turns out that using unique_ptr with a type-erased deleter is not uncommon in the industry. It does not have to be as sophisticated as something that calls into memory_resource. A deleter that takes a pointer to a polymorphic base class is possibly a type-erased deleter. Even &std::free is a legitimate type-erased deleter. So when designing the APIs to cast between unique_ptrs, we would like to support both expectations: one set of APIs to cast unique_ptr<T> to unique_ptr<U> and the other set to cast unique_ptr<T, D> to unique_ptr<U, D>. In other words, one defaults the deleter, and the other one preserves the deleter.
However, the set of casts we can safely perform in practice is not without boundaries with both API styles. According to our preliminary survey using GitHub code search, static_casts between unique_ptrs using the .release() trick almost always perform downcast in the hope of gaining performance over dynamic_cast by sacrificing safety, and reinterpret_casts between unique_ptrs only retrieve byte sequences. The authors consider both use cases require expertise and cannot be a part of the intuitive APIs, which are supposed to be safe by default. On the other hand, type-erased APIs for these types of casts would not only be expert-only but also serve no use case, as we know so far. Therefore, this paper proposes only dynamic_pointer_cast and const_pointer_cast between unique_ptrs.
Attention to safety is also reflected in the proposed APIs. For example, the API to dynamic cast unique_ptr<T> to unique_ptr<U> requires U to have a virtual destructor or to support destroying delete. This requirement doesn't apply to the deleter-preserving API where the deleter's behavior is unknown. But when the deleter is known to be default_delete<U>, we can prevent undefined behavior ahead of time. In some cases, prior knowledge of the default deleter reduces the amount of checks. For example, unique_ptr<T[], D>::pointer may also be T* if D is not default_delete<T[]>, so extra checks are employed to prevent accidentally creating unique_ptr that manages new[]-ed resources with a non-array deleter. The following chart summarizes the guardrails in the proposed APIs beyond the underlying calls to the unique_ptr constructors.
|
unique_ptr<T> unique_ptr<U> |
unique_ptr<T,D> unique_ptr<U,D> |
const_pointer_cast |
Valid to const_cast from T* to U* |
Valid to const_cast from unique_ptr<T, D>::pointer to unique_ptr<U, D>::pointer; Either T and U both are array types, or neither |
dynamic_pointer_cast |
Valid to dynamic_cast from T* to U*;
U has a virtual destructor or supports destroying delete |
Valid to dynamic_cast from unique_ptr<T, D>::pointer to unique_ptr<U, D>::pointer; Neither T nor U is an array type |
Technical Specification
template<class T, class U>
constexpr unique_ptr<T> dynamic_pointer_cast(unique_ptr<U>&& r) noexcept;
Constraints: dynamic_cast<T*>((U*)nullptr) is a valid expression.
Mandates:
has_virtual_destructor_v<T> is true, or
- the selected deallocation function of the expression
delete p for a p of type T* is a destroying operator delete ([basic.stc.dynamic.deallocation]).
Preconditions: The expression dynamic_cast<T*>(r.get()) has well-defined behavior.
Effects: Equivalent to:
if (auto p = dynamic_cast<T*>(r.get()))
return (void)r.release(), unique_ptr<T>(p);
else
return nullptr;
[Note 1: The seemingly equivalent expression unique_ptr<T>(dynamic_cast<T*>(r.get())) can result in undefined behavior, attempting to delete the same object twice. –end note]
template<class T, class D, class U>
constexpr unique_ptr<T, D> dynamic_pointer_cast(unique_ptr<U, D>&& r) noexcept;
Constraints: dynamic_cast<unique_ptr<T, D>::pointer>(declval<typename unique_ptr<U, D>::pointer>())) is a valid expression.
Mandates: Neither T nor U is an array type.
Preconditions: The expression dynamic_cast<unique_ptr<T, D>::pointer>(r.get()) has well-defined behavior.
Effects: Equivalent to:
if (auto p = dynamic_cast<unique_ptr<T, D>::pointer>(r.get()))
return (void)r.release(), unique_ptr<T, D>(p, std::forward<D>(r.get_deleter()));
else if constexpr (!is_pointer_v<D> && is_default_constructible_v<D>)
return nullptr;
else if constexpr (is_copy_constructible_v<D>)
return unique_ptr<T, D>(nullptr, r.get_deleter());
template<class T, class U>
constexpr unique_ptr<T> const_pointer_cast(unique_ptr<U>&& r) noexcept;
Constraints: const_cast<T*>((U*)nullptr) is a valid expression.
Effects: Equivalent to: return unique_ptr<T>(const_cast<T*>(r.release()));
[Note 2: The seemingly equivalent expression unique_ptr<T>(const_cast<T*>(r.get())) can result in undefined behavior, attempting to delete the same object twice. –end note]
template<class T, class D, class U>
constexpr unique_ptr<T, D> const_pointer_cast(unique_ptr<U, D>&& r) noexcept;
Constraints: const_cast<unique_ptr<T, D>::pointer>(declval<typename unique_ptr<U, D>::pointer>()) is a valid expression.
Mandates: is_array_v<T> == is_array_v<U> is true.
Effects: Equivalent to: return unique_ptr<T, D>(const_cast<unique_ptr<T, D>::pointer>(r.release()), std::forward<D>(r.get_deleter()));
Implementation Experience
Here is a full implementation: 51sjEjKcc
The snippet below implements the variant of dynamic_pointer_cast that preserves the deleter type (i.e., supports type-erased deleter).
template<class T, class D, class U>
requires(requires(unique_ptr<U, D>::pointer p) {
dynamic_cast<unique_ptr<T, D>::pointer>(p);
})
constexpr auto dynamic_pointer_cast(unique_ptr<U, D>&& r) noexcept
-> unique_ptr<T, D>
{
static_assert(!is_array_v<T> && !is_array_v<U>,
"don't work with array of polymorphic objects");
if (auto p = dynamic_cast<unique_ptr<T, D>::pointer>(r.get()))
{
r.release();
return unique_ptr<T, D>(p, std::forward<D>(r.get_deleter()));
}
else if constexpr (!is_pointer_v<D> && is_default_constructible_v<D>)
{
return {};
}
else if constexpr (is_copy_constructible_v<D>)
{
return unique_ptr<T, D>(nullptr, r.get_deleter());
}
else
{
static_assert(false, "unable to create an empty unique_ptr");
}
}
Acknowledgements
Thank Broadcom for supporting the work.
References
Jordan Saxonberg <jordan.saxonberg@broadcom.com>
Pointer cast for
unique_ptrChanges
Introduction
We propose
unique_ptroverloads forstd::const_pointer_castandstd::dynamic_pointer_cast. For each kind of cast, we allow users to choose between either using the defaulted deleter or preserving the original deleter type for each kind of cast.The table following illustrates the simpler case of the two where users expect defaulted deleters in the resulting types.
Given an API:
C++23
⚠ Owning raw pointer
❌ Leak resources if
dynamic_castfailsP3139
✔
✔
Prior Art
Boost.SmartPtr ships all four casts (
dynamic_pointer_cast,static_pointer_cast,const_pointer_cast, andreinterpret_pointer_cast) that createstd::unique_ptr<T>by releasingstd::unique_ptr<U>since 2016.Motivation
unique_ptrs was spotted in code reviews from independent parties. Without coincidence, the conclusions were to use.release(), a resource-unsafe API, as a one-off solution. Such a practice encourages the use of unsafe constructs and potentially breaks future code as a result. The standard C++ should encourage the opposite.std::dynamic_pointer_cast,std::const_pointer_cast, etc. These names resembledynamic_castandconst_castand work forstd::shared_ptralready. There is no simpler way to express the same intent and no reason to find a different set of names.dynamic_pointer_cast(std::unique_ptr<T>&&)on a yearly basis, albeitboost::dynamic_pointer_cast(std::unique_ptr<T>&&)existed before the group's birth. Some of the work supports preserving the incoming deleter type. It's time to consider adopting the working parts from Boost and explore the recurring extension.Design
unique_ptrdiffers fromshared_ptrin a few significant ways. Obviously, we can only cast from an rvalue ofunique_ptrby moving its ownership. The other differences that made impacts on the design are:shared_ptr<T>carries a type-erased deleter, whileunique_ptr<T, D>'s deleter is a part of the type. The seemly intuitiveunique_ptr<T>tounique_ptr<U>actually requires replacingstd::default_delete<T>withstd::default_delete<U>, which may not apply to the customized deleters.shared_ptr<U>is only an alias to the originalshared_ptr<T>. The newly created object requires no deleter, and you can expect the original deleter to be able to delete the uncasted pointer. Meanwhile,unique_ptr<U, D>, in general, must deal with the question "whetherDcan delete the casted pointer."shared_ptr<T>owns a "real" pointer, whileunique_ptr<T, D>can customize its pointer type as indicated by its pointer typedef. Converting between the pointer types may create loopholes as theunique_ptr<T[], D>specializations reused this mechanism.It turns out that using
unique_ptrwith a type-erased deleter is not uncommon in the industry. It does not have to be as sophisticated as something that calls intomemory_resource. A deleter that takes a pointer to a polymorphic base class is possibly a type-erased deleter. Even&std::freeis a legitimate type-erased deleter. So when designing the APIs to cast betweenunique_ptrs, we would like to support both expectations: one set of APIs to castunique_ptr<T>tounique_ptr<U>and the other set to castunique_ptr<T, D>tounique_ptr<U, D>. In other words, one defaults the deleter, and the other one preserves the deleter.However, the set of casts we can safely perform in practice is not without boundaries with both API styles. According to our preliminary survey using GitHub code search,
static_casts betweenunique_ptrs using the.release()trick almost always perform downcast in the hope of gaining performance overdynamic_castby sacrificing safety, andreinterpret_casts betweenunique_ptrs only retrieve byte sequences. The authors consider both use cases require expertise and cannot be a part of the intuitive APIs, which are supposed to be safe by default. On the other hand, type-erased APIs for these types of casts would not only be expert-only but also serve no use case, as we know so far. Therefore, this paper proposes onlydynamic_pointer_castandconst_pointer_castbetweenunique_ptrs.Attention to safety is also reflected in the proposed APIs. For example, the API to dynamic cast
unique_ptr<T>tounique_ptr<U>requiresUto have a virtual destructor or to support destroying delete[1]. This requirement doesn't apply to the deleter-preserving API where the deleter's behavior is unknown. But when the deleter is known to bedefault_delete<U>, we can prevent undefined behavior ahead of time. In some cases, prior knowledge of the default deleter reduces the amount of checks. For example,unique_ptr<T[], D>::pointermay also beT*ifDis notdefault_delete<T[]>, so extra checks are employed to prevent accidentally creatingunique_ptrthat managesnew[]-ed resources with a non-array deleter. The following chart summarizes the guardrails in the proposed APIs beyond the underlying calls to theunique_ptrconstructors.unique_ptr<T>unique_ptr<U>unique_ptr<T,D>unique_ptr<U,D>const_pointer_castconst_castfromT*toU*const_castfromunique_ptr<T, D>::pointertounique_ptr<U, D>::pointer;Either
TandUboth are array types, or neitherdynamic_pointer_castdynamic_castfromT*toU*;Uhas a virtual destructor or supports destroying deletedynamic_castfromunique_ptr<T, D>::pointertounique_ptr<U, D>::pointer;Neither
TnorUis an array typeTechnical Specification
Implementation Experience
Here is a full implementation: 51sjEjKcc
The snippet below implements the variant of
dynamic_pointer_castthat preserves the deleter type (i.e., supports type-erased deleter).Acknowledgements
Thank Broadcom for supporting the work.
References
Szolnoki, Lénárd. P2413R1 Remove unsafe conversions of unique_ptr<T>. https://wg21.link/p2413r1 ↩︎