|
pjson 2.0.0
A small, owning JSON value for C++11
|
Most applications should use pjson's built-in allocator. If an embedded system, arena, pool, or allocation tracer needs control over persistent DOM storage, implement pjson::Allocator and bind values to it. Follow along with examples/src/09_custom_allocator.cpp.
Without an allocator argument, a value uses pjson's built-in allocator:
The direct value is owned by its C++ scope. The parse result is a plain pjson value returned by every DOM parse overload, so it is also released automatically when it goes out of scope. Children returned by find() or findPointer() are borrowed views into the owning tree—never delete them yourself.
Every pjson value is allocator-bound, including default-constructed values. A custom allocator changes where selected persistent DOM allocations come from; it does not change the tree's ownership model.
Derive from the runtime interface and implement both operations:
The contract is:
allocate returns non-null storage satisfying the requested byte size and alignment, or throws (normally std::bad_alloc). Returning nullptr is not a supported failure signal.deallocate receives the original pointer and matching size, alignment, and kind. It must not throw.AllocationKind lets a pool maintain separate free lists or statistics:
| Kind | Persistent allocation represented |
|---|---|
NodeAllocation | A dynamically owned pjson node, including a parsed root |
StringAllocation | The std::string wrapper for a string-valued node |
ArrayAllocation | The internal wrapper for an array-valued node |
ObjectAllocation | The internal wrapper for an object-valued node |
ImplementationAllocation | The private representation of a non-null pjson value |
ImplementationAllocation is part of ABI generation 2; custom allocators must accept every defined kind and should avoid fixed-size tables that assume only the original four values. The hook deliberately does not replace every allocation in the process. The internal buffers/nodes allocated by std::string, std::vector, and std::unordered_map, and transient parsing, serialization, pointer, patch, and validation workspaces, continue to use the standard allocator.
A directly constructed root remains owned by the place where it was created:
Parsing must allocate the root's descendants dynamically, but every overload returns the document by value, bound to the supplied allocator:
The returned value is bound to pool: its wrapper objects and descendants were obtained from pool, and its destructor returns them through pool. There is no smart pointer and no manual delete. Moving the value transfers the tree but does not own or extend the allocator's lifetime.
An allocator-configured parser accepts std::string, (const char*, size_t), and std::istream, with optional pJsonParser::Error. Its Options are fixed at construction. parseStream() uses standard allocation for its temporary input buffer but uses the supplied allocator for the persistent DOM. SAX parsing builds no persistent DOM; the parser's allocator is unused by SAX calls.
Allocator provenance is part of a value's lifetime contract:
| Operation | Allocator behavior |
|---|---|
pjson copy(source) | Deep copy using source's allocator |
pjson copy(source, destinationAllocator) | Deep copy into the named allocator |
destination = source / copyFrom(source) | Deep copy while preserving the destination allocator |
pjson moved(std::move(source)) | O(1) transfer with the source allocator; source becomes null |
pjson moved(std::move(source), destinationAllocator) | O(1) when allocators match; otherwise deep-transfer, then source becomes null |
destination = std::move(source) | Preserves the destination allocator; same-allocator storage transfer may still destroy the old destination tree, while cross-allocator transfer may allocate |
left.swap(right) | O(1) only when left.canSwap(right); otherwise a safe no-op |
Check compatibility whenever two values may have come from different allocator domains:
Do not infer allocator ownership from equality or value type. Use &value.getAllocator() when provenance matters. A successfully moved-from source is JSON null but remains bound to its original allocator.
Allocator-aware in-memory parsing catches failures during DOM construction, destroys partial trees, returns a JSON null value, and fills pJsonParser::Error when supplied. parseStream() first fills a standard-allocated input buffer, so an exception-enabled stream or failure in that buffer can still throw before DOM construction.
Other operations that allocate—such as string/container mutation, deep copy, and cross-allocator move—may propagate std::bad_alloc. Copy assignment, cross-allocator move assignment, resetTo, string replacement, missing-key insertion, and array growth preserve existing data in their documented/tested failure paths. JSON Patch and Merge Patch are noexcept; allocation or internal failures are reported through false and PatchError, and the target is left unchanged.
An allocator should remain usable while failed operations unwind, because pjson may need it to release partially constructed nodes.
The companion example implements a small counting allocator using global operator new/delete. It is intentionally an instrumentation example, not an arena implementation:
pjson value.Allocator is borrowed and must outlive the entire bound tree.parse().canSwap() distinguishes the O(1) same-allocator path from a cross-allocator no-op.Return to the tutorial index, or consult the browsable API reference.