Prompt and Scope
A C++ service creates many small, short-lived objects per request and destroys them together. The current implementation calls new and delete frequently, and latency jitters. Compare std::pmr::monotonic_buffer_resource, the default allocator, and a pool resource; show key code and state when the choice is unsafe.
This is a coding question about allocator semantics, object lifetime, and measurable trade-offs. Assume objects are used by one request thread and the resource can be destroyed when the request ends.
What the Interviewer Evaluates
- Whether you can explain the runtime-polymorphic
memory_resourceboundary and container types. - Whether you understand that a monotonic resource grows and normally does not reclaim individual objects.
- Whether you bind resource lifetime to a request rather than the whole process.
- Whether you catch dangling references, early destruction, cross-thread use, and exception paths.
- Whether benchmarks prove the benefit through allocations, tail latency, and peak memory.
Clarifying Questions Before Answering
- Do all objects die with the request? Long-lived objects require a separate resource.
- Is individual reclamation, free-block reuse, or a hard memory cap required? A pool may fit better.
- Do containers and elements use the same
memory_resource? Nested strings and allocator-aware types must propagate it. - Will another thread, an async task, or a caller receive the container? This determines safe destruction.
- Is the bottleneck allocation calls, lock contention, cache locality, or another I/O/algorithm cost?
30-Second Answer Framework
I first confirm a batch lifetime. If everything can be discarded at request end, a monotonic resource can start with an initial buffer, obtain larger blocks from an upstream resource, and release them together when destroyed; it fits short-lived, append-like allocation. If individual reclamation or long reuse is required, I choose a pool or the default resource. I place the resource in request scope, destroy PMR users first, and benchmark latency, allocation count, and peak memory under the same load.
Step-by-Step Deep Dive
1. Draw the resource and object lifetimes
monotonic_buffer_resource allocates through the memory_resource interface. It starts with a caller-provided buffer and asks its upstream resource for more blocks when needed. Releasing one object normally does not return storage upstream; bulk release happens at destruction or an explicit release(). The resource therefore must outlive every container and element that uses it.
2. Match the allocation pattern
Request parse trees, temporary ASTs, and serialization intermediates fit a batch-create/batch-destroy pattern. Frequent release and reuse of fixed-size objects points to unsynchronized_pool_resource; sharing across threads requires a synchronized design or per-thread resources. new_delete_resource is simpler when the pattern is unstable or optimization evidence is weak.
3. Propagate the resource into nested objects
Replacing the outer container with a PMR container is not enough. If an element contains std::string, a child container, or an allocator-aware constructor, use the matching PMR type or uses-allocator construction. Otherwise inner objects may allocate from a different resource and invalidate the measurement.
4. Express ownership in minimal code
#include <array>
#include <memory_resource>
#include <string>
#include <vector>
struct RequestArena {
std::array<std::byte, 64 * 1024> initial{};
std::pmr::monotonic_buffer_resource resource{initial.data(), initial.size()};
std::pmr::vector<std::pmr::string> names{&resource};
};
void handle_request() {
RequestArena arena;
arena.names.emplace_back("temporary", &arena.resource);
}The initial buffer only reduces upstream allocations; total memory is not capped at 64 KiB. The resource can request more blocks, so production code should observe upstream bytes, enforce a request budget, and test destruction order on exceptions.
5. Handle release, exceptions, and returned values
If a request must reset midway, clear the containers and call release(), then stop using references to old objects. Never return a container whose resource scope ends. Normal stack unwinding gives the desired order: containers and elements destruct before the resource. Extending the resource lifetime dynamically increases leak and concurrency risk.
6. Close with a benchmark
Under identical inputs, compiler options, and thread settings, compare the default resource, monotonic, and pool. Record allocations per request, P50/P99 latency, peak RSS, total upstream bytes, cancellation cleanup time, and cross-request retention. If a lifetime mismatch grows the peak, revert or split the resource even when allocation calls decline.
High-Quality Sample Answer
I first verify that all these objects become invalid at request end. If so, a monotonic resource fits: it starts from an initial buffer, obtains blocks upstream as needed, skips individual reclamation, and releases the batch when the request ends. That reduces many small allocation and free calls, but does not impose a fixed memory cap and is unsafe for objects that must be reclaimed separately.
I put the resource in request scope, point PMR containers and allocator-aware elements to it, and destroy users before the resource. I switch to a pool for fixed-size reuse, use synchronized or per-thread resources across threads, and keep the default resource when evidence is insufficient. Before rollout I compare P99, allocation counts, peak memory, and cancellation cleanup under the same workload, including resource escape, exception unwinding, and high-volume requests.
Common Mistakes
Treating monotonic as an automatic memory cap
It continues asking upstream for blocks. Set a budget, observe upstream allocations, and reject or batch work when the budget is exceeded.
Keeping users after resource destruction
Their internal pointers dangle. Make the resource owner enclose every user and forbid returning them beyond that scope.
Replacing only the outer container
Nested strings may still use the default resource. Check allocator-aware construction and every nested PMR type.
Claiming a speedup without a benchmark
Allocator changes can be hidden by I/O or lock contention. Fix the workload and record latency, peak memory, and allocation counts.
Follow-Ups and Responses
Follow-up 1: Why not call deallocate for each element?
Bulk release is the design trade-off. Per-object reclamation would defeat the simple monotonic model; use a pool or the default resource for fine-grained release.
Follow-up 2: How large should the initial buffer be?
Estimate common request sizes from production distributions, leave exception headroom, and observe upstream requests. Too large wastes stack or resident memory; too small increases upstream allocation.
Follow-up 3: Can one monotonic resource be shared across threads?
Do not concurrently use an unsynchronized resource without protection. Prefer per-thread/per-request resources or an explicitly synchronized upstream design with verified lifetime.
Follow-up 4: How do you prove there is no cross-request leak?
Run a repeated request sequence and compare per-request peaks, cumulative upstream bytes, and RSS trend. Confirm destruction and recovery after cancellation, exceptions, and oversized requests.