Умные указатели
Документация движка. Эта страница определяет словарь native C++-указателей из
Source/Essentials/SmartPointers.h: владение, nullability, правила миграции и требования к проверке.
Назначение
Движок использует небольшие обёртки, чтобы контракт указателя был виден на
уровне типа. Долгосрочное соглашение соответствует модели nullability в
скриптах: отсутствие должно быть явным. Для non-null типа null не является
нормальным состоянием; nullable-тип использует написание с n*.
Переход от старого имени raw_ptr выполнялся поэтапно. raw_ptr<T> и
nullable_raw_ptr<T> удалены из API движка; используйте ptr<T> и nptr<T>.
nptr<T>, unique_nptr<T> и refcount_nptr<T> являются отдельными nullable
wrapper types. Обычные ptr<T>, unique_ptr<T> и refcount_ptr<T> всегда
подчиняются строгому non-null контракту; nullable-состояние принадлежит
соответствующему типу n*.
Словарь указателей
| Смысл | Non-null написание | Nullable написание |
|---|---|---|
| Невладеющий borrowed pointer | ptr<T> |
nptr<T> |
| Уникально владеющий pointer | unique_ptr<T> |
unique_nptr<T> |
| Владеющий intrusive-refcount pointer | refcount_ptr<T> |
refcount_nptr<T> |
| Уникальный pointer на массив | unique_arr_ptr<T> |
Будущий unique_arr_nptr<T>, если понадобится |
| Уникальный pointer с custom deleter | unique_del_ptr<T> |
unique_del_nptr<T> |
raw_ptr<T> / nullable_raw_ptr<T> являются устаревшими написаниями. Новый
код движка должен применять ptr<T> или nptr<T>.
Контракты
ptr<T> является невладеющим borrowed pointer. В пригодном для использования
состоянии он non-null, не имеет обычного пути присваивания/проверки nullptr и
не должен моделировать optional-состояние.
nptr<T> является nullable borrowed pointer. Используйте его для lookup,
текущего/активного/выбранного состояния, отсутствующих backend handles и
результатов API, где отсутствие нормально.
Оставляйте проверенное nullable-значение в nptr<T> и разыменовывайте его
напрямую. Не создавайте копию nullable_x и не сужайте её до ptr<T> только
ради вызова. У nptr<T> есть operator-> / operator* с тем же unchecked
deref, что у ptr<T>, поэтому после guard прямое разыменование бесплатно, а
чистое доменное имя остаётся у самого nptr<T>:
nptr<Critter> target = engine->GetCritter(id); // clean domain name stays on the nptr
if (!target) {
return; // guard proves non-null for the rest of the scope
}
target->Foo(); // deref the nptr directly — no as_ptr(), no copy
Guard может быть ранним if (!target) { <recover>; return; }, положительным
if (target) { ... }, short-circuit target && target->Foo(), if-init,
условием цикла или завершающим при ошибке FO_VERIFY_AND_THROW(target, "...")
/ FO_STRONG_ASSERT(target, "..."). Каждый вариант доказывает non-null, и
as_ptr() лишь добавил бы повторный assert. В условиях verify/assert пишите
truthiness указателя напрямую: target, size == 0 || data; если присутствие
сравнивается с другим bool, вычислите его явно через
static_cast<bool>(target) для wrapper или raw != nullptr для raw pointer.
Подключающий проект может проверять правило guard-aware анализатором над Engine
и native extensions. Репозиторий Engine владеет правилом и wrapper/runtime
тестами, но пока не поставляет такой анализатор. Проектная проверка
NullableLocalDereference должна исправлять каждый hit в исходнике, а не
скрывать его count baseline.
Проверенный nptr<T> неявно сужается до ptr<T>: не пишите .as_ptr() там,
где ожидается ptr<T>. Преобразующий конструктор проверяет non-null в точке
преобразования тем же always-on assert, что и .as_ptr(), поэтому проверенное
значение напрямую передаётся в параметр, поле или return типа ptr<T>:
nptr<Map> map = EntityMngr.GetMap(map_id); // clean domain name on the nptr
if (!map) {
return; // guard proves non-null
}
FindPath(map, cr, from_hex, to_hex); // FindPath takes ptr<Map> — implicit narrow, no .as_ptr()
Преобразование распространяет const и сохраняет mutability: из
const nptr<T> получается только ptr<const T>. Если nullable source хранится
в deduced local, а дальше требуется присутствие, сохраняйте сам wrapper и
проверяйте его до deref или неявной передачи в ptr<T>. Не полагайтесь на
скрытый assert внутри .as_ptr():
auto target = engine->GetCritter(id);
FO_VERIFY_AND_THROW(target, "Target critter not found");
target->Foo(); // checked nptr, direct deref
UseTarget(target); // UseTarget takes ptr<Critter>
Явные .as_ptr() / .as_nptr() уместны, когда показывают borrow, разрешают
overload/template deduction, материализуют значение после move владельца или
создают копируемый lambda capture. Если destination type уже однозначен,
оставляйте неявное преобразование.
Для raw T* используйте глобальный helper и deduced local:
auto target = make_ptr(raw_target); // raw T* -> ptr<T>, asserts non-null
auto maybe_target = make_nptr(raw_target); // raw T* -> nptr<T>, preserves null
Владеющие wrapper заимствуются так же: refcount_ptr<T> /
refcount_nptr<T>, unique_ptr<T> / unique_nptr<T>, unique_del_ptr<T> /
unique_del_nptr<T>, unique_arr_ptr<T> и shared_ptr<T> неявно переходят в
borrow-site ptr<T> / nptr<T> через get(). Преобразование в ptr<T>
проверяет non-null, а в nptr<T> сохраняет отсутствие; владение не передаётся.
В call/typed-return/member site с известным destination type передавайте owner
напрямую. Для локального доступа используйте исходный owner или auto&. Только
если deduction, move owner или lambda capture действительно требуют отдельный
borrow, создавайте auto borrow = owner.as_ptr(); / auto maybe_borrow =
owner.as_nptr(); и проверяйте nullable до dereference. Если nullable owner
только что получен из фабрики с проверенным non-null результатом и нужен для
нескольких обращений, работайте с ним напрямую.
Если временный borrow нужен только для dynamic cast, вызывайте dyn_cast<T>()
у owner: _views[i].dyn_cast<EditorAssetView>(), а не
_views[i].as_ptr().dyn_cast<EditorAssetView>(). Cast у unique_ptr<T> /
unique_nptr<T> возвращает borrowed nptr<U>. refcount_ptr<T> /
refcount_nptr<T> возвращают владеющий refcount_nptr<U>, если U сам
intrusive-refcountable, и borrowed nptr<U> для mixin/interface без refcount.
Намеренное получение владения из borrow пишется явно: hold_ref() /
try_hold_ref() или require_refcount_ptr(...).
Обратные переходы от borrowed wrapper к owner остаются явными и проверяемыми:
hold_ref() / try_hold_ref() для intrusive refs,
adopt_unique_ptr(ptr<T>) для scalar unique adoption,
make_unique_del_ptr(...) для custom deleter, take_not_null() для narrowing
nullable owner и safe_alloc::make_shared(...) / domain factory для shared
ownership. Неявного перехода ptr<T> / nptr<T> в owner нет.
Если суженный ptr<T> действительно сосуществует с nullable в одной области,
называйте их по роли: чистое доменное имя получает рабочее значение, а boundary
value существует только для проверки или narrowing:
| Тип | local / parameter (snake_case) |
data member (_camelCase) |
|---|---|---|
raw T* |
raw_target |
_rawTarget |
nptr<T>, сужаемый в сосуществующий ptr<T> |
maybe_target / доменное имя |
_maybeTarget / доменное имя |
ptr<T> (non-null, narrowed) |
target |
_target |
// raw C-ABI pointer -> checked non-null
static void Cleanup(sentry_options_t* raw_options) noexcept {
if (raw_options != nullptr) { // check the raw pointer directly
ptr<sentry_options_t> options = raw_options; // clean name for the non-null ptr
sentry_options_free(options.get());
}
}
Обычный случай оставляет чистое имя проверенному nptr<T>, поэтому не вводите
nullable_ как ритуал narrowing, не придумывайте _lookup, _ref, _ptr или
_begin для каждого вызова и не копируйте именованный nullable/raw pointer
только ради префикса. После != nullptr связывайте raw pointer сразу с
ptr<T>, без промежуточного nptr<T>.
Заведомо non-null значение, например std::string::data() /
string_view::data(), &object или уже non-null результат, связывайте прямо с
ptr<T> без проверки и nptr<T>. Не создавайте wrapper для локального pointer,
который не покидает функцию и нужен только для address arithmetic; достаточно
ptr<T> или raw pointer для чистой математики:
ptr<const char> view_begin = _sv.data(); // data() is never null — no nptr, no check
ptr<const char> storage_begin = _s.data();
ptr<const char> storage_end = storage_begin.get() + _s.size();
if (view_begin < storage_begin || !(view_begin < storage_end)) { ... }
Исключение: exported/ABI parameter с именем, закреплённым внешним контрактом или
проектным allowlist, сохраняет имя; сужайте его в local <name>_ptr.
То же именование относится к unique_nptr<T>::take_not_null(),
refcount_nptr<T>::take_not_null() и custom-deleter narrowing.
unique_ptr<T> владеет одним объектом в usable state. Moved-from объект может
быть пуст только до destruction или reassignment. Если пустота нормальна для
модели, используйте unique_nptr<T>.
unique_nptr<T> владеет нулём или одним объектом и подходит для lazy resources,
optional owned state и объектов, создаваемых после owner или очищаемых до его
уничтожения. Проверенный nullable owner переносится в unique_ptr<T> через
unique_nptr<T>::take_not_null(), явно проверяющий invariant. Для custom
deleter используйте take_not_null(unique_del_nptr<T>&); helper переносит и
сохранённый deleter.
В strict contract unique_ptr<T>::release() и
unique_del_ptr<T>::release() возвращают ptr<T>, а nullable-owner release
сохраняет nptr<T> или raw ABI storage. Разворачивайте .get() только на
явной ABI-, allocator- или adoption-границе. Helper, у которого type cast или
lookup может не сработать при передаче unique ownership, возвращает
unique_nptr<T>: неудачный cast является нормальным отсутствием.
unique_del_ptr<T> / unique_del_nptr<T> используются на внешних cleanup
boundaries. Non-null вариант допустим после доказательства присутствия и не
принимает default/null construction; nullable-вариант нужен для пустого, lazy,
moved-from или resettable состояния. Typed C array/buffer индексируйте через
сам owner (owner[index]), без временного borrow; void owner намеренно не
поддерживает indexing. Непрозрачный C resource можно хранить прямо как
unique_del_ptr<void> / unique_del_nptr<void>, если cleanup принимает
соответствующий void*.
refcount_ptr<T> владеет non-null intrusive reference: копирование увеличивает
счётчик, destruction уменьшает. refcount_nptr<T> хранит reference при наличии
и может быть пуст; он подходит для optional entity/view/current state и lookup,
которому нужно удержать найденный объект. Borrow выполняется неявно, а
refcount_nptr<T>::take_not_null() переносит проверенное владение в
refcount_ptr<T> без дополнительного add-ref.
Не оборачивайте nullable intrusive ownership в
optional<refcount_ptr<T>>; используйте refcount_nptr<T>. Результат загрузки,
различающий «отсутствует» и «ошибка», возвращает refcount_nptr<T> вместе с
отдельным error flag, например bool& is_error. Аналогично избегайте
optional<ptr<T>>, optional<nptr<T>>, optional<unique_ptr<T>>,
optional<unique_nptr<T>> и optional<refcount_nptr<T>>; используйте прямой
словарь wrapper или именованный domain result type.
shared_ptr<T> и weak_ptr<T> являются собственными shared-ownership типами
движка, без std::shared_ptr внутри. Atomic control block владеет объектом через
strong count и собой через weak count; объект размещён в той же allocation
через safe_alloc::make_shared(), а virtual destruction hook не требует полного
pointee type у holder. Типы с shared_from_this() / weak_from_this()
наследуются от enable_shared_from_this<T>; factory подключает embedded weak
reference после construction, поэтому использовать его в constructor нельзя.
Member casts: shared_ptr<U>::dyn_cast<T>() разделяет control block и пуст при
неудаче; shared_ptr<const T>::cast_no_const() является явным escape hatch.
Присутствующий shared owner неявно заимствуется в ptr<T>, потенциально пустой
в nptr<T>; owner должен жить весь период borrow. unique_arr_ptr<T> и
unique_del_nptr<T> тоже принадлежат движку, а
unique_del_nptr<T>::get_deleter() открывает deleter для проверяемой передачи.
Состояние class/struct не должно хранить C++ reference members (T& _member
или const T& _member). Параметры constructor и короткие local alias могут
оставаться reference. Обязательная stored borrowed dependency хранится в
проверенном ptr<T>, optional dependency в nptr<T>, unique_nptr<T> или
refcount_nptr<T>. Value member применяется только для действительно
принадлежащих объекту данных.
Аргументы процесса/runtime входят через ABI argc / argv и сразу переходят
в wrapper vocabulary. Для внутренних view используйте CommandLineArg /
CommandLineArgs; временный char** создавайте только перед последним ABI
handoff.
Точка входа вне SDL владеет аргументами через ProgramArgs (Source/Common/Common.h): в Windows он читает UTF-8 из широкой командной строки вместо argv в ANSI code page и выдаёт CommandLineArgs view на срок своей жизни.
На проверенной raw cleanup boundary применяйте именованный helper вместо owner
construction из .get(): adopt_unique_ptr(ptr<T>) для scalar object и
make_unique_del_ptr(ptr<T>, deleter) /
make_unique_del_ptr(nptr<T>, deleter) для custom deleter. Domain helper может
оборачивать эти primitives, но call site не должен писать
unique_ptr<T> {value.get()} или unique_del_ptr<T> {value.get(), deleter}.
Для low-level byte/ABI reinterpret pointee type используйте
ptr<T>::reinterpret_as<U>() / nptr<T>::reinterpret_as<U>(), а не roundtrip
через raw void*. Результат сохраняет nullability и const, поддерживая
ptr<void> source.
ptr<T>::void_cast() / nptr<T>::void_cast() являются односторонним C/ABI
handoff и возвращают raw void*. Concrete typed nullable borrow восстанавливает
cast_from_void<T*>(void_ptr). Для indirection вида void** в AngelScript
handle-slot plumbing переинтерпретируйте wrapper через
ptr<void>::reinterpret_as<void*>() или
nptr<const void>::reinterpret_as<const void*>(). Уже обёрнутый source не
оборачивайте повторно. Concrete-to-concrete storage reinterpret выполняйте
через явный deduced borrow; raw source сначала связывайте make_ptr /
make_nptr.
Mutable byte span превращается в typed span через
bytes_to_objects<T>(span<uint8_t>), который проверяет кратность размера и
использует reinterpret_as; обратный object_to_bytes<T>(T&) предоставляет
span<uint8_t> одного объекта.
Typed element span из borrow pointer и length создавайте через
make_span(ptr<T>, length) / make_const_span(ptr<T>, length), чтобы .get()
оставался внутри helper. Raw overload make_const_span(const T*, n) подходит
для container data. Другие overload make_span считают байты. Для текстового
ptr<char> / ptr<const char> используйте as_str(size_t len).
Always-on проверка non-null
Non-null invariant проверяется всегда, во всех build configuration, а не
debug-only assert. Construction ptr<T> из null raw pointer, каждый borrow
nullable/owner в ptr<T> и каждый ownership narrowing проверяют invariant; при
нарушении синхронно записываются expression, source location и native stack,
после чего процесс завершается.
Проверка называется FO_BASIC_STRONG_ASSERT(expr) и полностью принадлежит
Essentials/FatalError.h/.cpp, расположенному после StackTrace /
BaseLogging и до SmartPointers в строгом cascade Essentials. Поэтому
SmartPointers зависит только вверх и не обращается к более позднему
ExceptionHandling. noexcept reporter использует ранний native fatal path,
а не создаёт StrongAssertationException, поэтому безопасен из noexcept
members. Любой module Essentials после FatalError может использовать тот же
primitive; код на уровне BasicCore, GlobalData, StackTrace или
BaseLogging обязан применять механизм своего слоя.
Следствие: null внутри ptr<T> завершает процесс в точке construction даже в
release. Частые источники: .data() пустого контейнера, способный вернуть null,
и raw API с null как «не найдено». Реально nullable значение храните в
nptr<T>; transient buffer, используемый один раз, можно передать напрямую в
consumer с nptr/raw.
Мосты refcount
Переход raw или borrowed pointer во владение intrusive refcount всегда явный:
refcount_ptr<Entity> held = entity_ptr.hold_ref();
refcount_nptr<Entity> maybe_held = maybe_entity.try_hold_ref();
ptr<Entity> borrowed = held;
nptr<Entity> maybe_borrowed = maybe_held;
Если raw pointer неизбежен на ABI, atomic или allocator boundary, используйте именованные factories:
refcount_ptr<Entity> held_from_raw = refcount_ptr<Entity>::from_add_ref(raw_entity);
refcount_nptr<Entity> maybe_held_from_raw = refcount_ptr<Entity>::try_from_add_ref(raw_entity);
refcount_ptr<Entity> adopted = refcount_ptr<Entity>::from_adopted_ref(raw_entity_with_existing_ref);
Прямые refcount_ptr<T>(T*), operator=(T*) и public adopt_tag недоступны,
чтобы raw/refcount переход был заметен при review.
Контракт non-null и явных мостов
Эти правила безусловны. Старые FO_STRICT_* migration flags удалены; существует
только strict behavior.
- Прямые raw construction/assignment
refcount_ptr<T>и publicadopt_tagнедоступны; используйте именованные factories. - У
ptr<T>нет default/null construction, assignment/comparison сnullptr,operator bool,get_pp()илиreset()без replacement. - У
unique_ptr<T>/unique_del_ptr<T>/refcount_ptr<T>нет normal empty state. Ихrelease()сохраняет non-null wrapper shape; nullable owners сохраняют nullable/raw ABI форму.
Любые два wrapper сравниваются напрямую через heterogeneous operator==:
пишите item == other_item, а не .get() с обеих сторон. Проектный анализатор
может называть вторую форму DirectWrapperGetComparison, но Engine-owned
контракт задают операторы wrapper и Test_SmartPointers.cpp.
Allowlist raw pointer
Не навязывайте wrapper там, где raw pointer яснее отражает ABI или low-level представление:
void*и byte buffer с соседним size;- internals allocator, placement new/delete и pointer arithmetic;
- C, OS, graphics, COM-style и third-party ABI;
- process/runtime
argc/argvи последнийchar**handoff; - generic AngelScript plumbing, registration strings, handle slots, low-level
char*/char**; script-visible handle signatures используют wrapper; std::atomic<T*>до появления специального atomic nullable wrapper.
Во всех остальных Engine-owned API используйте ptr<T> / nptr<T> для borrow
и unique_* / refcount_* для владения.
Граница script binding (FO_SCRIPT_API)
Binding ///@ ExportMethod создаёт BuildTools/codegen.py, и каждый
script-visible handle в generated ABI использует ptr<T> / nptr<T>, включая
///@ ExportEvent, FO_ENTITY_EVENT, ///@ ExportRefType и
///@ EngineHook. Codegen запрещает bare raw handle pointers, а
NativeDataProvider / NativeDataCaller содержат static assert.
- Receiver engine/entity всегда non-null и записывается
ptr<EngineType>/ptr<EntityType>. - Non-null argument/return использует
ptr<T>, nullable используетnptr<T>. - Container elements также используют wrapper:
vector<ptr<T>>,readonly_vector<ptr<T>>/readonly_vector<nptr<T>>. Только metadata AngelScript сводит element к raw handle, сохраняя registered signature и compatibility hash; generated extern иNativeCallсохраняют C++ wrapper. - Методы
///@ ExportRefTypeиспользуют wrapper для return и generatedptr<RefType>receiver; script metadata и compatibility hash не меняются. - Регистрация AngelScript не меняется, поэтому миграция
.fos, bytecode или save не нужна; изменяется только C++ glue type.
Raw на ABI edge намеренно остаются generic AngelScript plumbing, handle slots,
char* / char**, process argv и сторонние C callback. В export body связывайте
raw input с wrapper до обычной работы и вызывайте .get() только в последней
handoff line. Если declared return type сам raw pointer, return wrapper.get();
допустим; если функция возвращает wrapper, возвращайте wrapper. То же относится
к get_no_const() на последней mutable script ABI edge и placement-new в
GetAddressOfReturnLocation().
Правила миграции
- Замените legacy
raw_ptr<T>наptr<T>без изменения поведения. - Замените legacy
nullable_raw_ptr<T>наnptr<T>. - Пустые
ptr<T> ... {}переведите вnptr<T>. - Пустые
unique_ptr<T> ... {}переведите вunique_nptr<T>. - Пустые
refcount_ptr<T> ... {}переведите вrefcount_nptr<T>. - Проверьте lookup API, current-state fields и boundary adapters до tightening.
- Передавайте проверенный
nptr<T>прямо вptr<T>site; отдельный.as_ptr()нужен только для самостоятельного borrow. - Для ownership narrowing применяйте
take_not_null()соответствующего owner. - Заимствуйте refcount owner неявно; сужайте ownership только явно.
- Вызывайте owner
dyn_cast<T>()напрямую. - Return, способный вернуть
nullptr/{}, должен иметь nullable type. - Не возвращайте удалённые permissive
FO_STRICT_*modes; при интеграции старой ветки сначала перенесите optional state в nullable wrappers.
Проверка
Во время staged migration используйте grep gates:
rg "\braw_ptr<" Source SourceExt
rg "^\s*(mutable\s+)?ptr<[^\n;]+>\s+\w+[^;{}]*\{\}\s*;" Source SourceExt
rg "^\s*(mutable\s+)?unique_ptr<[^\n;]+>\s+\w+[^;{}]*\{\}\s*;" Source SourceExt
rg "^\s*(mutable\s+)?refcount_ptr<[^\n;]+>\s+\w+[^;{}]*\{\}\s*;" Source SourceExt
После pointer-layer изменений запускайте generated engine unit-test target подключающего проекта. При изменении native bindings или exported signatures дополнительно выполняйте script compilation/baking validation проекта.
Engine пока не поставляет whole-tree textual/AST smart-pointer checker. Подключающий проект может хранить migration guards, но его commands, task names, baselines и allowlists не являются нормативным доказательством движка.
Проектный checker должен применять точные line-level allowlists только для
проверенных ABI/low-level boundaries, а не count budgets; напрямую запрещать
class reference members и unguarded nullable dereference; отличать inventory
nullable owner от violation; использовать настоящий compile_commands.json
для AST. Перенесите checker в BuildTools/ и добавьте Engine-owned tests прежде,
чем объявлять его стандартной командой этой документации.