Document #: | P3401R1 [Latest] [Status] |
Date: | 2025-10-06 |
Project: | Programming Language C++ |
Audience: |
LEWGI LEWG |
Reply-to: |
Tian Liao <tian.lt@outlook.com> Mingxin Wang <mingxwa@outlook.com> |
proxiable_target
inplace_proxiable_target
make_proxy_inplace
allocate_proxy
allocate_proxy_shared
make_proxy_shared
make_proxy
make_proxy_view
Abstract
This paper proposes standard library factory functions that construct
proxy<F>
(and proxy_view<F>
)
objects with clear ownership and storage semantics: adaptive value
construction (make_proxy
), forced
in-place (make_proxy_inplace
),
allocator-supplied (allocate_proxy
),
compact shared ownership
(make_proxy_shared
/
allocate_proxy_shared
), and
non-owning view creation
(make_proxy_view
). Two new concepts,
proxiable_target
and
inplace_proxiable_target
, provide
uniform, diagnosable constraints on value acceptability for these
creation paths. Centralizing these facilities eliminates duplicated ad
hoc small-buffer wrappers, allocator holders, and reference-counted
pointer types; it yields consistent diagnostics, enables trivial
relocation optimization, and makes allocation and borrowing intent
explicit.
proxiable_target
to distinguish any pointer-like type already modeling
proxiable
from a value type that can
be wrapped as an acceptable target through an observing or owning
witness.facade F
where previously
class F
appeared.allocate_proxy_shared
,
make_proxy_shared
) and non-owning
view creation
(make_proxy_view
).make_proxy_inplace
,
make_proxy_view
) remain available in
freestanding; allocation-enabled functions are
freestanding-deleted.proxiable_target
/
inplace_proxiable_target
rather than
substitution failures with less direct messaging.This paper proposes to standardize a minimal, performance‑oriented
set of creation facilities for the pointer‑semantics polymorphism
facility (P3086: class template
proxy
and its facade model). The
goal is to let users express (1) ownership intent, (2) storage strategy,
and (3) allocator choice explicitly, without re‑implementing small
buffer optimization (SBO), reference counting handles, or view wrappers
ad hoc. These factories are intentionally narrow: they neither expose
user‑visible control blocks nor introduce new abstraction layers beyond
what is required to build an efficient pointer‑like witness.
Two supporting concepts provide uniform, diagnosable constraints over value types passed to the factories:
proxiable_target<T, F>
:
a value type that can be turned into some pointer‑like witness
acceptable to facade F
(observing or
owning path).inplace_proxiable_target<T, F>
:
a value type whose object can reside directly (in‑place) within a
witness subject to facade size/alignment and lifetime constraint
levels.Proposed additions (all in <memory>
):
proxiable_target
,
inplace_proxiable_target
.make_proxy
(decides in‑place vs
allocation).make_proxy_inplace
(compile‑time
rejection if impossible).allocate_proxy
(explicit allocator parameter, single object construction only).allocate_proxy_shared
,
make_proxy_shared
.make_proxy_view
producing proxy_view<F>
.To use proxy
effectively, users
today must hand craft one or more of:
shared_ptr
(with a separately
allocated control block whose shape is outside facade constraint
reasoning).Each local reinvention increases risk of ABI drift and subtle performance regressions (e.g. unintentional dynamic allocation where an in‑place object would have satisfied the facade). Consolidating these patterns as standard factories concentrates optimization effort and yields consistent diagnostics.
The proposal adds two concepts and six families of function templates
to <memory>
.
No existing interfaces change. The feature test macro
__cpp_lib_proxy
is updated. No
deprecations are introduced. ABI impact is limited to additional
exported (or inline) templates; internal layout of
proxy
is unchanged.
Freestanding status:
make_proxy_inplace
,
make_proxy_view
,
proxiable_target
,
inplace_proxiable_target
.make_proxy
,
allocate_proxy
,
make_proxy_shared
,
allocate_proxy_shared
(dynamic
allocation / reference counting).make_proxy
chooses between
in-place storage and heap allocation at compile time using facade
constraint parameters and target type traits. This mirrors existing
practice in widely deployed implementations and shields users from
reimplementing small buffer handles incorrectly. A separate
make_proxy_inplace
exists to force
compile-time rejection when in-place storage is not possible, providing
a clear intent signal for latency-sensitive code that wants to rule out
allocation.
Before introducing
proxiable_target
, value-type
acceptability errors surfaced indirectly through template instantiations
of helper wrappers. proxiable_target<T, F>
declaratively states: a value of type
T
can be made into a pointer-like
witness acceptable to facade F
via
an observing or owning handle. This improves diagnostics (ill-formed
with required messages) and enables consistent constraints across
allocating, shared, and observing creation functions.
The shared ownership creation functions employ exposition-only
strong-compact-ptr / weak-compact-ptr types whose size
and alignment never exceed those of a raw pointer. This ensures facade
layout constraints remain satisfied when users intend reference counting
but still desire compact handles (important for cache-sensitive
polymorphic graphs). The deleter used by the internal shared handle is a
concrete, non-erased deleter selected at compile time; type-erasing the
deleter would add an avoidable level of indirection and storage
overhead. Destruction polymorphism (invoking the correct destruction
behavior for the erased value type) is already provided by
proxy
itself, so erasing the deleter
yields no additional semantic power.
make_proxy_view
produces a proxy_view<F>
that borrows a referenced object. Using a dedicated function clarifies
that no ownership transfer or allocation occurs, and allows a direct
constraint based on
proxiable_target
. The selected
witness (observer-ptr) is trivial and can always be trivially
copied and relocated, preserving facade constraint levels.
In-place and view construction are retained in freestanding because
they can be implemented without dynamic allocation and do not require
reference counting machinery. Allocation and shared ownership rely on
dynamic storage and are therefore freestanding-deleted,
matching the base proxy
approach to
allocator-dependent facilities.
This section contains proposed wording additions to <memory>
.
Exposition‑only entities have no linkage.
In [version.syn], update:
#define __cpp_lib_proxy YYYYMML // also in <memory>
The value is updated to the date of adoption of this paper.
namespace std {
// concepts
template<class T, class F>
concept proxiable_target = see below;
template <class T, class F>
concept inplace_proxiable_target = see below;
// value-based creation (freestanding-deleted)
template<facade F, class T, class... Args>
<F> make_proxy(Args&&... args); // freestanding-deleted
proxytemplate<facade F, class T, class U, class... Args>
<F> make_proxy(initializer_list<U> il, Args&&... args); // freestanding-deleted
proxytemplate<facade F, class T>
<F> make_proxy(T&& value); // freestanding-deleted
proxy
// allocator-supplied creation (freestanding-deleted)
template<facade F, class T, class Alloc, class... Args>
<F> allocate_proxy(const Alloc& alloc, Args&&... args); // freestanding-deleted
proxytemplate<facade F, class T, class Alloc, class U, class... Args>
<F> allocate_proxy(const Alloc& alloc, initializer_list<U> il, Args&&... args); // freestanding-deleted
proxytemplate<facade F, class Alloc, class T>
<F> allocate_proxy(const Alloc& alloc, T&& value); // freestanding-deleted
proxy
// in-place forced creation (freestanding)
template<facade F, class T, class... Args>
<F> make_proxy_inplace(Args&&... args) noexcept(is_nothrow_constructible_v<T, Args...>)
proxyrequires(is_constructible_v<T, Args...>);
template<facade F, class T, class U, class... Args>
<F> make_proxy_inplace(initializer_list<U> il, Args&&... args)
proxynoexcept(is_nothrow_constructible_v<T, initializer_list<U>&, Args...>)
requires(is_constructible_v<T, initializer_list<U>&, Args...>);
template<facade F, class T>
<F> make_proxy_inplace(T&& value)
proxynoexcept(is_nothrow_constructible_v<decay_t<T>, T>)
requires(is_constructible_v<decay_t<T>, T>);
// shared ownership creation (freestanding-deleted)
template<facade F, class T, class Alloc, class... Args>
<F> allocate_proxy_shared(const Alloc& alloc, Args&&... args); // freestanding-deleted
proxytemplate<facade F, class T, class Alloc, class U, class... Args>
<F> allocate_proxy_shared(const Alloc& alloc, initializer_list<U> il, Args&&... args); // freestanding-deleted
proxytemplate<facade F, class Alloc, class T>
<F> allocate_proxy_shared(const Alloc& alloc, T&& value); // freestanding-deleted
proxy
template<facade F, class T, class... Args>
<F> make_proxy_shared(Args&&... args); // freestanding-deleted
proxytemplate<facade F, class T, class U, class... Args>
<F> make_proxy_shared(initializer_list<U> il, Args&&... args); // freestanding-deleted
proxytemplate<facade F, class T>
<F> make_proxy_shared(T&& value); // freestanding-deleted
proxy
// view creation (freestanding)
template<facade F, class T>
<F> make_proxy_view(T& value) noexcept;
proxy_view}
The following exposition-only types are used in the specifications below:
inplace-ptr
: same
size and alignment as T
; contains
storage for a T
; provides operator*
with matching cv/ref qualifications; no empty state.allocated-ptr<T, Alloc>
:
manages dynamically allocated storage for a single
T
constructed with
Alloc
; provides operator*
.strong-compact-ptr<T, Alloc>
:
reference-counted owning handle; size and alignment not greater than
those of a raw pointer; conditionally convertible to
weak-compact-ptr<T, Alloc>
.weak-compact-ptr<T, Alloc>
:
non-owning handle referencing the same control block as a
strong-compact-ptr
.observer-ptr
:
trivially copyable wrapper of a raw
T*
; provides
operator*
.proxiable_target
template<class T, class F>
concept proxiable_target = see below;
proxiable_target<T, F>
is equivalent to proxiable<observer-ptr
.
inplace_proxiable_target
template<class T, class F>
concept inplace_proxiable_target = see below;
inplace_proxiable_target<T, F>
is equivalent to proxiable<inplace-ptr
.
For each function template family (make_proxy, allocate_proxy, allocate_proxy_shared, make_proxy_shared, make_proxy_view, make_proxy_inplace)
with declarations numbered (1-3)
:
T
other than
via in-place construction, the program is ill-formed if !proxiable_target<decay_t<T>, F>
.!inplace_proxiable_target<decay_t<T>, F>
.!proxiable_target<T, F>
.Ill-formed conditions require a diagnostic; they do not participate in overload resolution via substitution failure.
make_proxy_inplace
// (1)
template<facade F, class T, class... Args>
<F> make_proxy_inplace(Args&&... args)
proxynoexcept(is_nothrow_constructible_v<T, Args...>)
requires(is_constructible_v<T, Args...>);
// (2)
template<facade F, class T, class U, class... Args>
<F> make_proxy_inplace(initializer_list<U> il, Args&&... args)
proxynoexcept(is_nothrow_constructible_v<T, initializer_list<U>&, Args...>)
requires(is_constructible_v<T, initializer_list<U>&, Args...>);
// (3)
template<facade F, class T>
<F> make_proxy_inplace(T&& value)
proxynoexcept(is_nothrow_constructible_v<decay_t<T>, T>)
requires(is_constructible_v<decay_t<T>, T>);
Effects: (1)
Creates a proxy<F>
containing a value p
of type
inplace-ptr
where
*p
is
direct-non-list-initialized with std::forward<Args>(args)...
.
(2)
Same but with il
as the first
argument to the construction of
*p
. (3)
Same as (1)
with T
replaced by decay_t<T>
and the single argument std::forward<T>(value)
.
Remarks: No dynamic allocation is performed. The functions are
noexcept
as
specified.
allocate_proxy
// (1)
template<facade F, class T, class Alloc, class... Args>
<F> allocate_proxy(const Alloc& alloc, Args&&... args); // freestanding-deleted
proxy// (2)
template<facade F, class T, class Alloc, class U, class... Args>
<F> allocate_proxy(const Alloc& alloc, initializer_list<U> il, Args&&... args); // freestanding-deleted
proxy// (3)
template<facade F, class Alloc, class T>
<F> allocate_proxy(const Alloc& alloc, T&& value); // freestanding-deleted proxy
Effects: (1)
Creates a proxy<F>
containing a value p
of type
allocated-ptr<T, Alloc>
where *p
is
direct-non-list-initialized with std::forward<Args>(args)...
.
(2)
Same with il, std::forward<Args>(args)...
.
(3)
Same with T
replaced by decay_t<T>
and argument std::forward<T>(value)
.
Throws: Any exception thrown by allocation or construction of
T
. May allocate additional storage
for a copy of alloc
if required by
the facade constraints.
allocate_proxy_shared
// (1)
template<facade F, class T, class Alloc, class... Args>
<F> allocate_proxy_shared(const Alloc& alloc, Args&&... args); // freestanding-deleted
proxy// (2)
template<facade F, class T, class Alloc, class U, class... Args>
<F> allocate_proxy_shared(const Alloc& alloc, initializer_list<U> il, Args&&... args); // freestanding-deleted
proxy// (3)
template<facade F, class Alloc, class T>
<F> allocate_proxy_shared(const Alloc& alloc, T&& value); // freestanding-deleted proxy
Effects: As for allocate_proxy
but the contained value is of type strong-compact-ptr<X, Alloc>
(where X
is
T
or decay_t<T>
).
The construction of X
and any
control block are treated as a single operation for exception
safety.
Throws: Any exception thrown by allocation or by construction of
X
.
make_proxy_shared
// (1)
template<facade F, class T, class... Args>
<F> make_proxy_shared(Args&&... args); // freestanding-deleted
proxy// (2)
template<facade F, class T, class U, class... Args>
<F> make_proxy_shared(initializer_list<U> il, Args&&... args); // freestanding-deleted
proxy// (3)
template<facade F, class T>
<F> make_proxy_shared(T&& value); // freestanding-deleted proxy
Effects: (1)
Equivalent to return allocate_proxy_shared<F, T>(std::allocator<void>{}, std::forward<Args>(args)...);
(2)
Equivalent to the same with il
.
(3)
Equivalent to (1)
with T
replaced by decay_t<T>
.
make_proxy
The definitions of make_proxy
use
an exposition-only function template:
template<facade F, class T, class... Args>
<F> __make_proxy_internal(Args&&... args) {
proxyif constexpr (inplace_proxiable_target<T, F>) {
return make_proxy_inplace<F, T>(std::forward<Args>(args)...);
} else {
return allocate_proxy<F, T>(std::allocator<void>{}, std::forward<Args>(args)...);
}
}
// (1)
template<facade F, class T, class... Args>
<F> make_proxy(Args&&... args); // freestanding-deleted
proxy// (2)
template<facade F, class T, class U, class... Args>
<F> make_proxy(initializer_list<U> il, Args&&... args); // freestanding-deleted
proxy// (3)
template<facade F, class T>
<F> make_proxy(T&& value); // freestanding-deleted proxy
Effects: (1)
Equivalent to return __make_proxy_internal<F, T>(std::forward<Args>(args)...);
(2)
Equivalent to return __make_proxy_internal<F, T>(il, std::forward<Args>(args)...);
(3)
Equivalent to return __make_proxy_internal<F, decay_t<T>>(std::forward<T>(value));
.
Throws: Any exception thrown by allocation or construction of
T
.
make_proxy_view
template<facade F, class T>
<F> make_proxy_view(T& value) noexcept; proxy_view
Effects: Creates a proxy_view<F>
containing a value p
of type
observer-ptr
where
*p
is
direct-non-list-initialized with
&value
.
Remarks: No allocation; always
noexcept
.
In-place and view creation functions only propagate exceptions thrown by construction of the target value (view creation cannot throw). All other creation functions may additionally throw due to allocation failure.
Each function has O(1)
time complexity with respect to the number of forwarded arguments.
Reference counting initialization for shared forms is O(1)
.
Implementations may perform trivial relocation when the facade’s relocatability constraint level permits and the witness type satisfies the corresponding requirements.
The proposed factories supply:
make_proxy
) enabling SBO without
user code.make_proxy_inplace
) to reject
accidental allocation.make_proxy_view
) clarifying
lifetime without re‑specifying facade operations.Centralizing these patterns in the standard library removes duplicated bespoke wrappers, improves portability of optimizations, and clarifies performance contracts around allocation and ownership for pointer‑semantics polymorphism.