API Reference — pma/ScopedPtr
pma::DefaultInstanceCreator<T>
Type trait that resolves the default creator policy for T. Evaluates to New<T> out of the box; specialize it to override the default for a specific type.
Why this exists
makeScoped<T>(args...) needs to know which creator to use without requiring the caller to spell it out every time. DefaultInstanceCreator<T>::type provides that default in one specializable place. If a type later switches from operator new to a factory pattern, specializing this trait — rather than updating every call site — keeps all makeScoped<T>() uses correct automatically.
Fields
| Name | Type | Description |
|---|---|---|
type |
New<T> |
The resolved creator type. Access via DefaultInstanceCreator<T>::type. |
Construction
// Consume via trait lookup (typical)
using Creator = pma::DefaultInstanceCreator<MyClass>::type; // == pma::New<MyClass>
Creator creator;
MyClass* obj = creator(arg1, arg2);
// Specialize to override the default for a given type:
namespace pma {
template<>
struct DefaultInstanceCreator<MyPooledClass> {
using type = FactoryCreate<MyPooledClass>;
};
}
Relationships
DefaultInstanceDestroyer<T>— the parallel trait for the destroyer sideNew<T>— the default resolved typemakeScoped— queries this trait when no explicit creator is provided
pma::DefaultInstanceDestroyer<T>
Type trait that resolves the default destroyer policy for T. Evaluates to Delete<T> out of the box; specialize it to change the destruction path for a specific type without touching its call sites.
Why this exists
ScopedPtr<T> uses DefaultInstanceDestroyer<T>::type as its default TDestroyer. This means you can globally change how T is destroyed — switching from delete to a factory T::destroy() call — by specializing this one trait. All ScopedPtr<T> and makeScoped<T>() usages then pick up the correct destroyer automatically.
Fields
| Name | Type | Description |
|---|---|---|
type |
Delete<T> |
The resolved destroyer type. Access via DefaultInstanceDestroyer<T>::type. |
Construction
// Consume via trait lookup:
using Destroyer = pma::DefaultInstanceDestroyer<MyClass>::type; // == pma::Delete<MyClass>
// Specialize to override:
namespace pma {
template<>
struct DefaultInstanceDestroyer<MyPooledClass> {
using type = FactoryDestroy<MyPooledClass>;
};
}
// Now ScopedPtr<MyPooledClass> automatically calls MyPooledClass::destroy()
Relationships
DefaultInstanceCreator<T>— the parallel trait for the creator sideDelete<T>— the default resolved typeScopedPtr— reads this trait as its defaultTDestroyerparameter
pma::Delete<T, B>
Deallocate a B* via operator delete, enforcing at compile time that T is a complete type. The array specialization Delete<T[]> uses delete[].
Why this exists
Calling delete on an incomplete type is undefined behavior in C++, but the compiler only warns — it does not error. Delete uses a complete_type_checker typedef (char[sizeof(T) ? 1 : -1]) that produces a compile error for incomplete types, making the UB impossible to reach silently. This is the safety contract that makes ScopedPtr safe to use with forward-declared types as long as the destroyer is instantiated where T is complete.
Fields
| Name | Type | Description |
|---|---|---|
T |
template type param | The concrete type being destroyed. Must be a complete type at the point where Delete<T>::operator() is instantiated. |
B |
template type param | The base pointer type accepted; defaults to T. |
Construction
// Single-object form — called by ScopedPtr automatically
pma::Delete<MyClass> destroyer;
destroyer(ptr); // equivalent to: delete ptr (with completeness check)
// Array form
pma::Delete<MyClass[]> array_destroyer;
array_destroyer(arr); // equivalent to: delete[] arr
Watch out for
Delete<T>::operator()must be instantiated in a translation unit whereTis fully defined. IfTis forward-declared in the header that ownsScopedPtr<T>, move theScopedPtrdestructor definition to a.cppwhereTis complete. The compile error fromcomplete_type_checkersignals exactly this problem.
Relationships
New<T>— the companion creator;Delete<T>is the corresponding destroyerDefaultInstanceDestroyer<T>— trait that resolves toDelete<T>ScopedPtr— usesDelete<T>as its defaultTDestroyerFactoryDestroy<T>— alternative destroyer for types that use a staticT::destroy()factory pattern
pma::FactoryCreate<T, B>
Create a T by calling T::create(args...) and return the result as B*. Use this as the creator policy for ScopedPtr when T controls its own allocation via a static factory method.
Why this exists
Many C++ types in this codebase (and plugin/module boundaries in general) expose a static T* create(…) / static void destroy(T*) pair rather than public constructors, because they manage their own memory allocator or need to return a different concrete subtype. FactoryCreate adapts that pattern to the ScopedPtr policy interface so that factory-managed objects participate in the same scoped lifetime system as heap-allocated objects.
Fields
| Name | Type | Description |
|---|---|---|
T |
template type param | The concrete type whose static create() method is called. Must have static B* T::create(Args...). |
B |
template type param | The base type returned; defaults to T. |
Construction
// Pair with FactoryDestroy and use via makeScoped:
auto ptr = pma::makeScoped<MyService, pma::FactoryCreate, pma::FactoryDestroy>();
// ptr is ScopedPtr<MyService, FactoryDestroy<MyService>>
// constructed via MyService::create(), destroyed via MyService::destroy()
Relationships
FactoryDestroy<T>— the mandatory companion destroyer; always pair these twoNew<T>— the alternative creator for types usingoperator newmakeScoped— acceptsFactoryCreateandFactoryDestroyas template-template args
pma::FactoryDestroy<T, B>
Release a B* by calling T::destroy(static_cast<T*>(ptr)). Always pair with FactoryCreate<T> when owning objects that use a static factory allocation pattern.
Why this exists
When a type owns its memory via T::create() / T::destroy(), calling delete on the pointer would bypass the type's deallocation path — potentially leaking allocator state or custom heap resources. FactoryDestroy enforces that the correct destruction path is always taken, and it does so with zero overhead when used as a stateless policy in ScopedPtr.
Fields
| Name | Type | Description |
|---|---|---|
T |
template type param | The concrete type whose static destroy() method is called. Must have static void T::destroy(T*). |
B |
template type param | The base pointer type accepted; defaults to T. The cast static_cast<T*>(ptr) is applied before dispatch. |
Construction
pma::FactoryDestroy<MyService> destroyer;
destroyer(ptr); // calls MyService::destroy(static_cast<MyService*>(ptr))
// Typical usage — let makeScoped wire it automatically:
auto svc = pma::makeScoped<MyService, pma::FactoryCreate, pma::FactoryDestroy>();
Watch out for
- The
static_cast<T*>(ptr)inoperator()meansB*must be safely downcasted toT*. IfBis a virtual base or an unrelated type, the cast produces undefined behavior. Ensure the pointer was originally aT*before assigning it to aScopedPtr<B, FactoryDestroy<T, B>>.
Relationships
FactoryCreate<T>— always pair togetherDelete<T>— the alternative for types usingoperator deleteScopedPtr— stores this as itsTDestroyerpolicy
pma::makeScoped<T>(args...) / pma::makeScoped<T, TCreator, TDestroyer>(args...)
Construct a ScopedPtr<T> by invoking a creator policy and binding its matched destroyer — the preferred way to create any ScopedPtr.
When to use this
Use makeScoped instead of constructing ScopedPtr directly; it deduces the policy types, checks type compatibility via static_assert, and keeps call sites concise. Provide explicit TCreator/TDestroyer template-template arguments when the type uses a static factory (FactoryCreate / FactoryDestroy); omit them entirely when new/delete suffices.
Example
// Simplest form — uses DefaultInstanceCreator/DefaultInstanceDestroyer
auto mesh = pma::makeScoped<RigMesh>(numVertices, numFaces);
// mesh is ScopedPtr<RigMesh, Delete<RigMesh>>
// Factory-managed type
auto reader = pma::makeScoped<dna::StreamReader,
pma::FactoryCreate,
pma::FactoryDestroy>(stream, layer);
// calls dna::StreamReader::create(stream, layer)
// destroyed via dna::StreamReader::destroy()
// Explicit creator/destroyer types (non-template-template form)
using Creator = pma::FactoryCreate<dna::StreamReader, dna::Reader>;
using Destroyer = pma::FactoryDestroy<dna::StreamReader, dna::Reader>;
auto reader2 = pma::makeScoped<dna::StreamReader, Creator, Destroyer>(stream, layer);
Parameters
| Name | Type | Description |
|---|---|---|
T |
template type param | The concrete type to create. |
TCreator |
template type param | optional — Creator policy or template-template arg; defaults to DefaultInstanceCreator<T>::type. |
TDestroyer |
template type param | optional — Destroyer policy or template-template arg; defaults to DefaultInstanceDestroyer<T>::type. |
args |
Args&&... |
Arguments forwarded to the creator's operator(). |
Returns
ScopedPtr<Base, TDestroyer> — an owning smart pointer whose Base type is deduced from the creator's return type. For the simplest overload, Base == T.
Watch out for
- The full-explicit overload (
<T, TCreator, TDestroyer>) fires astatic_assertifTis not the same as, a base of, or pointer-convertible toBase(the type the creator actually returns). The error message is "Incompatible types." — check thatTCreator{}(args...)returns a pointer convertible toBase*. - When using template-template arguments (
makeScoped<T, FactoryCreate, FactoryDestroy>), both templates must accept a single type parameter. Custom policy templates with additional parameters require the explicit<T, Creator, Destroyer>form.
pma::New<T, B>
Allocate a T instance on the heap and return it as B*. Used as the default creator policy for ScopedPtr and makeScoped.
Why this exists
ScopedPtr separates object creation from lifetime management, letting you swap either half independently. New is the default creation half — it calls operator new T{args...} and is zero-cost when used as a stateless policy. The array specialization New<T[]> allocates with new T[size]{} (value-initialized), which is the safe default for raw arrays.
Fields
| Name | Type | Description |
|---|---|---|
T |
template type param | The concrete type to allocate. |
B |
template type param | The base type returned; defaults to T. Allows returning a base-class pointer from a derived allocation. |
Construction
// Single-object form
pma::New<MyClass> creator;
MyClass* obj = creator(arg1, arg2); // calls new MyClass{arg1, arg2}
// Array form
pma::New<MyClass[]> array_creator;
MyClass* arr = array_creator(16); // calls new MyClass[16]{} (value-initialized)
Relationships
Delete<T>— the companion destroyer; used alongsideNew<T>as the default destroyer policyDefaultInstanceCreator<T>— trait that resolves toNew<T>; use when selecting the creator via traitsScopedPtr— consumesNewas itsTDestroyer-paired creator viamakeScopedmakeScoped— factory that instantiatesNewautomatically when no creator is specified
pma::ScopedPtr<T, TDestroyer>
Take ownership of a raw pointer and automatically destroy it via TDestroyer when the ScopedPtr goes out of scope. Prefer makeScoped<T>(args...) over constructing ScopedPtr directly.
When to use this
Use ScopedPtr when you own a raw pointer that must be released through a specific destructor policy — particularly for types with a static T::destroy() factory (use FactoryDestroy) or custom allocators. Prefer std::unique_ptr for simple heap ownership with no policy requirements; reach for ScopedPtr when you need the swappable creator/destroyer policy system.
Description
ScopedPtr inherits from TDestroyer privately, making stateless destroyer policies (like Delete<T> or FactoryDestroy<T>) zero-cost via the empty base optimization. For stateful destroyers (lambdas with captures, custom allocator instances), pass the destroyer instance to the ScopedPtr(pointer, destroyer_type&&) constructor. Copy construction and copy assignment are deleted; only move semantics are supported. Cross-type moves are supported when the source pointer is convertible to the target pointer type.
Example
// Simple heap-allocated object (Delete<MyClass> destroyer is zero-cost)
auto ptr = pma::makeScoped<MyClass>(ctorArg1, ctorArg2);
// ptr is ScopedPtr<MyClass, Delete<MyClass>>
ptr->doWork();
// destroyed automatically on scope exit
// Factory-managed type
auto svc = pma::makeScoped<MyService, pma::FactoryCreate, pma::FactoryDestroy>();
// calls MyService::create(), destroys via MyService::destroy()
// Array form
auto buf = pma::makeScoped<float[]>(1024);
buf[0] = 1.0f;
// Stateful destroyer (e.g., pool allocator)
MyAllocator alloc;
auto pooled = pma::ScopedPtr<MyClass, MyAllocator>(
alloc.allocate<MyClass>(), std::move(alloc));
Parameters
| Name | Type | Description |
|---|---|---|
T |
template type param | The owned type (may be T[] for array ownership). |
TDestroyer |
template type param | optional — The destroyer policy; defaults to DefaultInstanceDestroyer<T>::type. Must be callable as TDestroyer()(pointer). |
ptr_ |
pointer |
The raw pointer to take ownership of. Must not be shared with any other owner. |
destroyer |
destroyer_type&& |
optional — A stateful destroyer instance; only for the (pointer, destroyer_type&&) constructor overload. |
Watch out for
ScopedPtrinheritsTDestroyerprivately, so the destroyer is invoked directly — do not pass a destroyer type whoseoperator()has preconditions that require it to be initialized after construction.- Moving from a
ScopedPtr<U, UDestroyer>toScopedPtr<T, TDestroyer>requires thatU*is implicitly convertible toT*. The static assertion inmakeScopedchecks this; the cross-type move constructor does not independently verify it. - After
release()the caller owns the pointer and is responsible for its destruction — theScopedPtrno longer manages it.
Relationships (see also)
makeScoped— preferred factory; avoids spelling out policy typesNew/Delete— default creator/destroyer policiesFactoryCreate/FactoryDestroy— policies for static-factory typesDefaultInstanceCreator/DefaultInstanceDestroyer— traits that resolve the defaults