1How do custom deleters work in smart pointers, and when are they useful for C/C++ interoperability?
A custom deleter is callable cleanup logic used by a smart pointer instead of the default delete operation when the pointer releases its owned resource. It is useful for C/C++ interoperability when a resource must be released with a specific function such as fclose, close, curl_easy_cleanup, SSL_free, free, or a library destroy function. For unique_ptr, the deleter type is part of the unique_ptr type and can affect its size; stateless deleters may be optimized away, while function-pointer or stateful deleters add storage. For shared_ptr, the deleter is stored in the control block and runs when the last strong owner releases the object. Custom deleters let C resources participate in RAII safely.
2Explain weak_ptr and shared_ptr control-block mechanics, including cycles, enable_shared_from_this, and refcount costs.
A shared_ptr manages shared ownership through a control block containing strong and weak reference counts plus cleanup information such as the deleter and allocator. Copying or destroying shared_ptr objects increments or decrements the strong count, typically with atomic operations, so separate shared_ptr objects can be manipulated safely across threads but each refcount update has cost. When the strong count reaches zero, the managed object is destroyed; the control block remains until weak references are also gone. weak_ptr points to the same control block without extending object lifetime; lock() returns a shared_ptr if the object is still alive and an empty shared_ptr otherwise. Cycles made only of shared_ptr leak because strong counts never reach zero, so weak_ptr is used for back-pointers or observer links. enable_shared_from_this lets an object already owned by shared_ptr create a new shared_ptr to itself using the existing control block, avoiding dangerous separate control blocks. Refcount costs include atomic increments/decrements, cache contention, control-block allocation, and overhead in hot paths.
3Explain C++ value categories and perfect forwarding, and why they matter for efficient generic APIs.
C++ value categories describe expressions: lvalues have identity and can be referred to after the expression; prvalues are pure rvalues such as many temporaries/computed values; xvalues are expiring objects whose resources may be reused. Perfect forwarding is the template technique of taking a forwarding reference, typically T&& where T is deduced, and forwarding with std::forward<T>(arg) so the caller's value category is preserved: lvalues remain lvalues and rvalues remain rvalues. Reference collapsing rules make this possible. It matters for generic backend APIs because wrappers, factories, dispatchers, and emplace-style functions can avoid unnecessary copies and preserve overload selection and move behavior.
4Compare auto, decltype, decltype(auto), and template argument deduction in common backend code.
auto uses template-like deduction for variables: plain auto usually drops references and top-level const unless the declaration asks for them, such as auto&, const auto&, or auto&&. decltype(expr) inspects the declared type or expression type more exactly: an unparenthesized id-expression gives the declared type, while other lvalue expressions produce T&, xvalues produce T&&, and prvalues produce T. decltype(auto) deduces using decltype rules, often for return types when references must be preserved. Template argument deduction is similar to auto but depends on the parameter form, such as T, T&, const T&, or T&&, and has its own rules. Braced initializers are a common difference: auto x = {1,2} deduces std::initializer_list<int>, while a plain template parameter generally cannot deduce T from a bare braced initializer unless the parameter expects an initializer_list or another suitable type.
5Compare std::variant with inheritance-based polymorphism for modeling heterogeneous messages or events.
std::variant is a value type that holds exactly one alternative from a fixed, closed set of types and is commonly handled with std::visit or explicit type queries. It is useful for protocol messages or events when the set of message kinds is known and you want type-safe handling without virtual dispatch and often without per-object heap allocation. Inheritance-based polymorphism uses a base class and virtual functions to dispatch through a common interface; it is better when the set of derived message types is open, independently extensible, plugin-like, or hidden behind a stable interface. Variant favors closed sum types, locality, and compile-time checking; inheritance favors extensibility, runtime polymorphism, and interface-based design.
6What is undefined behavior in C++, and how can it appear in backend production incidents?
Undefined behavior is behavior for which the C++ standard imposes no requirements after an invalid operation occurs. The program may appear to work, crash, corrupt data, expose security bugs, or be optimized into surprising behavior. Compilers assume UB does not happen and optimize based on that assumption, so issues may appear only in release builds or under production traffic. Backend incidents can come from dangling pointers/references, use-after-free, object lifetime violations, out-of-bounds access, signed integer overflow, data races, invalid casts, uninitialized reads, double frees, or strict-aliasing violations. Mitigation includes RAII and clear ownership/lifetime design, safer abstractions and bounds checks, testing/fuzzing, code review, static analysis, and sanitizers such as ASan, UBSan, and TSan.
7Differentiate undefined, unspecified, and implementation-defined behavior with backend-relevant examples.
Undefined behavior means the C++ standard imposes no requirements; results may include crashes, data corruption, security bugs, or optimizer-dependent miscompilation. Examples include out-of-bounds access, use-after-free, signed integer overflow, and data races. Unspecified behavior means the standard allows more than one outcome and the implementation does not have to document which one is chosen; a common example is the evaluation order of many function arguments, so code should not depend on side effects being evaluated in a particular order. Implementation-defined behavior means the implementation must choose and document the behavior; examples include the signedness of plain `char`, sizes/ranges of some fundamental types within standard limits, and some behavior of signed right shift. In backend code, UB is a correctness/security risk, while unspecified and implementation-defined behavior are portability risks that should be avoided or isolated in protocol, storage, and cross-platform logic.
8Explain exception safety levels and how noexcept affects move operations, containers, and code generation.
Exception safety levels describe what remains true if an operation throws. The basic guarantee means invariants are preserved and resources are not leaked, though state may have changed. The strong guarantee means commit-or-rollback semantics: on failure, the observable state is unchanged. The nothrow guarantee means the operation does not throw. Common techniques include RAII, performing work on temporaries, copy-and-swap, and ordering mutations so commit happens only after throwing work succeeds. `noexcept` is a contract: if a `noexcept` function throws, `std::terminate` is called. It also affects generic code and containers: for example, during reallocation `std::vector` can move elements when the move constructor is `noexcept`; otherwise it may copy, often via `std::move_if_noexcept`, to preserve exception guarantees. `noexcept` can also help code generation or optimization by reducing needed exception propagation paths, but only when the promise is correct.
9Explain std::expected or equivalent outcome types as alternatives to exceptions for fallible operations.
`std::expected<T, E>` represents either a successful value `T` or an explicit error `E`, making failure part of the function’s return type instead of relying on exception propagation. It is useful for fallible operations where errors are expected and callers should handle them locally, such as parsing, validation, network calls, storage lookups, and retryable backend operations. The error type should carry structured information such as an error code, category, retryability, message, or HTTP/RPC mapping. Expected/outcome types compose by checking and propagating errors, and in C++23-style APIs can use monadic operations such as `and_then`, `transform`, and `or_else` to chain steps without deeply nested conditionals. Compared with exceptions, they make control flow and API contracts explicit and work well across exception-free or ABI boundaries; exceptions may still be appropriate for rare, non-local, or truly exceptional failures depending on project policy.
10Describe object lifetime rules for automatic, dynamic, temporary, and asynchronously referenced objects.
Automatic objects live from construction until the end of their scope; pointers or references to them become dangling after that scope exits. Dynamic objects live from allocation/construction until they are explicitly destroyed or an owning object destroys them; raw pointers and references do not extend lifetime. Temporaries usually live until the end of the full expression, with specific lifetime-extension rules when bound to references, but not in every use. Asynchronous work such as callbacks, threads, timers, or coroutines may run after the referenced object has been destroyed, so captures and stored references need explicit lifetime management to avoid dangling references and use-after-free.
11Describe static initialization order issues and how constinit, magic statics, and dependency injection mitigate them.
Static initialization order issues occur because dynamically initialized namespace-scope or static objects in different translation units have unspecified relative initialization order. One global may use another before it has been constructed; destruction order can also create similar problems at shutdown. constinit forces a static or thread-local variable to have static/constant initialization or the program is ill-formed, avoiding dynamic initialization-order dependence for that variable. Magic statics, or function-local statics, are initialized on first use and are thread-safe since C++11. Dependency injection avoids hidden global dependencies by constructing objects in a controlled order and passing dependencies explicitly.
12What are copy elision, NRVO, and RVO, and when can you rely on them?
Copy elision means constructing an object directly in its final destination instead of creating a separate temporary and copying or moving it. RVO usually refers to returning an unnamed temporary or prvalue such as return T{}; in C++17 and later many of these cases are mandatory because the prvalue initializes the result object directly. NRVO is returning a named local object such as return x; the compiler is allowed to construct that local directly in the caller's return slot, but this is not guaranteed. You can rely on mandatory C++17 prvalue elision in the specified cases, but not on NRVO always happening; avoid std::move on a named local return value because it can prevent NRVO.
13How do comparator, equality, and hasher requirements affect correctness of associative containers?
Associative containers rely on comparison, equality, and hashing to define key identity and to maintain their internal invariants. Ordered containers such as std::map require the comparator to impose a strict weak ordering; keys are considered equivalent when neither compares less than the other, not necessarily by operator==. Unordered containers require equality to be an equivalence relation, and any two keys considered equal must produce the same hash value. Keys must not be mutated while stored in a way that changes their ordering, equality, or hash, because that can make lookup, erase, and uniqueness behavior incorrect.
14Explain heterogeneous lookup in ordered and unordered associative containers.
Heterogeneous lookup lets an associative container be searched using a type different from its key type, avoiding temporary key construction. In ordered containers, this requires a transparent comparator, for example std::less<> or a custom comparator with an is_transparent marker. In unordered containers, it requires both transparent hash and transparent equality functors that can handle the key type and lookup type consistently. A common backend example is a container keyed by std::string that can be searched with std::string_view or const char* without allocating a temporary std::string.
15Explain the C++ memory model: data races, happens-before, and synchronization guarantees.
The C++ memory model defines when operations in different threads are ordered and when writes become visible. A data race occurs when two threads access the same memory location concurrently, at least one access is a write, and the accesses are not ordered by happens-before or otherwise made safe by atomic operations; a data race causes undefined behavior. Happens-before is the ordering relation that makes earlier side effects visible to later operations. Synchronization operations create this ordering: for example, unlocking a mutex synchronizes-with a later successful lock of the same mutex, and suitable atomic release/acquire operations can synchronize between threads. Correct programs use mutexes, atomics, or other synchronization to establish happens-before for shared state.