Katkat

C++ Fundamentals: Memory, Compilation, and Containers

Hudson River TradingSoftware Engineer · Technical Screen

Last updated

Tests deep C++ expertise in zero-allocation data structures, cache-line alignment, manual object lifecycle management via placement new, and the compilation/inlining implications of template-based designs.

Problem Statement

In latency-critical trading systems, dynamic heap allocations (new/malloc) along the critical path introduce non-deterministic OS latency and memory fragmentation. Standard library containers such as std::queue (defaulting to std::deque) or std::vector often trigger heap allocations, cache misses due to pointer chasing, or expensive reallocations.

Design and implement a fixed-capacity, cache-friendly ring buffer template class FixedRingBuffer in modern C++ (C++17/20) that guarantees zero dynamic memory allocations throughout its entire lifetime.

Requirements

  1. Memory Layout & Storage:

    • Storage must be allocated inline inside the container (e.g., via an uninitialized aligned byte buffer alignas(alignof(T)) std::byte storage_[Capacity * sizeof(T)]).
    • Do not default-construct instances of T at container initialization.
    • Construct elements in-place using placement new with perfect forwarding in emplace_back(Args&&... args).
    • Explicitly call the destructor ~T() when elements are popped or when the buffer is destroyed.
  2. Interface:

    • bool push(const T& item) / bool push(T&& item): Inserts an element. Returns false if the buffer is full without overwriting.
    • template bool emplace(Args&&... args): In-place constructs the element. Returns false if full.
    • bool pop(T& out): Moves the front element into out, destructs the item in storage, advances the head, and returns true. Returns false if empty.
    • const T* front() const and T* front(): Returns a pointer to the head item, or nullptr if empty.
    • size_t size() const, bool empty() const, bool full() const.
  3. Compilation & Inlining:

    • All methods must be inlineable without cross-translation-unit overhead.
    • Ensure the class is noexcept where operations on T are noexcept.

Class Interface

template 
class FixedRingBuffer {
    static_assert(Capacity > 0, "Capacity must be greater than 0");
public:
    FixedRingBuffer() noexcept;
    ~FixedRingBuffer();

    // Non-copyable for safety
    FixedRingBuffer(const FixedRingBuffer&) = delete;
    FixedRingBuffer& operator=(const FixedRingBuffer&) = delete;

    template 
    bool emplace(Args&&... args);

    bool push(const T& val);
    bool push(T&& val);
    bool pop(T& out);

    T* front() noexcept;
    const T* front() const noexcept;

    size_t size() const noexcept;
    bool empty() const noexcept;
    bool full() const noexcept;
};

Example

FixedRingBuffer q;
assert(q.empty());

q.emplace("order_1");
q.push("order_2");
q.push("order_3");
assert(q.full());
assert(!q.push("order_4")); // rejected: full

std::string item;
assert(q.pop(item));
assert(item == "order_1");
assert(q.size() == 2);

Constraints & Edge Cases

  • 1 <= Capacity <= 1,000,000.
  • Non-trivial types: Types with deleted default constructors or custom destructors must be handled safely without memory leaks or undefined behavior.
  • Destruction: Any elements remaining in the buffer when ~FixedRingBuffer() runs must be cleanly destructed.

Hints

Hint 1

Use an array of `std::aligned_storage_t` or an explicit `alignas(T) std::byte` array to prevent premature object construction.

Hint 2

Maintain `head_`, `tail_`, and `count_` indices, advancing them modulo `Capacity` while ensuring `reinterpret_cast` points to valid initialized memory.

Hint 3

In the destructor, loop through the remaining initialized slots starting at `head_` and invoke `reinterpret_cast(...)->~T()` for exactly `count_` elements.

Follow-up questions

  • How would you eliminate the modulo operator (`%`) in the index calculation, and what compile-time constraint does that place on `Capacity`?
  • If this container were adapted into a Single-Producer Single-Consumer (SPSC) lock-free queue across two CPU threads, how would you prevent false sharing and ensure memory ordering?
  • How does the compiler handle template instantiation across multiple translation units, and why might defining template member functions in a `.cpp` file lead to linker errors?