HRT's Copy-on-Write String C++ Question
Hudson River Trading interviews lean hard on C++ internals. This copy-on-write string is a recurring on-site and phone question that quietly tests reference counting, value semantics, and the subtleties of sharing memory between instances, then makes you reason about threads. Here is the full question, C++ for each stage, and a detailed rubric.
The question
Implement a copy-on-write string class in C++. Multiple instances can share one underlying buffer; a modification copies the buffer only if it is currently shared; reference counts are tracked. Implement the constructor, copy constructor, assignment operator, operator[] in both const and non-const forms, and c_str.
Bonus: make it thread-safe.
Stage 1 · Shared buffer with reference counting
Put the character data and a reference count in one heap block. Copies share that block and bump the count; destruction drops it and frees only at zero. This gives cheap copies and correct lifetime.
#include <cstring>
#include <cstddef>
class CowString {
struct Buffer {
size_t refs;
size_t len;
char data[1]; // flexible-array-style trailing storage
};
Buffer* buf_;
static Buffer* alloc(const char* s, size_t len) {
Buffer* b = static_cast<Buffer*>(::operator new(sizeof(Buffer) + len));
b->refs = 1;
b->len = len;
std::memcpy(b->data, s, len);
b->data[len] = '\0';
return b;
}
void release() { if (buf_ && --buf_->refs == 0) ::operator delete(buf_); }
public:
CowString(const char* s = "") : buf_(alloc(s, std::strlen(s))) {}
CowString(const CowString& other) : buf_(other.buf_) { ++buf_->refs; } // share, do not copy
CowString& operator=(const CowString& other) {
if (this != &other) {
++other.buf_->refs; // guard self-assignment by incrementing first
release();
buf_ = other.buf_;
}
return *this;
}
~CowString() { release(); }
const char* c_str() const { return buf_->data; }
};
Stage 2 · Copy on mutation
Const access can hand back a reference into the shared buffer. Non-const access might be used to write, so it must detach first: if the buffer is shared, deep-copy it into a private one before returning a mutable reference. That is the whole point of copy-on-write.
// Detach: if the buffer is shared, make a private copy before mutating.
void detach() {
if (buf_->refs > 1) {
Buffer* fresh = alloc(buf_->data, buf_->len);
--buf_->refs; // we still hold a reference to the old one
buf_ = fresh;
}
}
char operator[](size_t i) const { return buf_->data[i]; } // read: no copy
char& operator[](size_t i) { // write path: must own the buffer first
detach();
return buf_->data[i];
}
The trap is returning a non-const reference without detaching: a write through it would corrupt every instance sharing the buffer. Note too that the non-const operator pessimistically copies even for a read, which is the known cost of this design.
Stage 3 · Thread safety
An atomic reference count is necessary but not sufficient. It makes share and release race-free, but two threads mutating views of the same logical string still need higher-level synchronization, and detach must atomically check-and-copy.
#include <atomic>
struct Buffer {
std::atomic<size_t> refs; // atomic so share/release are race-free
size_t len;
char data[1];
};
// release becomes:
void release() {
if (buf_ && buf_->refs.fetch_sub(1, std::memory_order_acq_rel) == 1)
::operator delete(buf_);
}
Say the quiet part: atomics protect the refcount, not the bytes. Concurrent writers to the same string need a mutex or a redesign, and the read-then-copy in detach has a race unless the count check and the copy are made atomic together.
How HRT scores it
| Dimension | Weak | Strong |
|---|---|---|
| Reference counting | Leaks or double-frees on copy or assign | One refcount per buffer, freed exactly at zero |
| Self-assignment | Frees then reads freed memory | Increments before releasing, or checks identity |
| Copy-on-write | Copies eagerly or never | Copies only when shared, and only on the write path |
| const vs non-const | Both accessors behave the same | const reads shared data, non-const detaches first |
| Memory safety | Off-by-one on the null terminator | Correct sizing and termination, no UB |
| Thread safety | Claims an atomic refcount is enough | Atomic count plus an honest account of writer races |
This is one question of 12 pages
Get the full Hudson River Trading question bank
The full HRT bank is heavy on C++ systems internals: copy-on-write, std::function, lock-free structures, and memory ordering, each with follow-ups and a rubric, compiled from people familiar with the process and cross-verified across sources.
Hudson River Trading interview FAQ
What does the Hudson River Trading interview ask?+
Deep C++ systems questions: implement copy-on-write, std::function, or a lock-free stack, plus memory ordering, atomics, scheduling, and deadlock. The signal is precise reasoning about value semantics, ownership, and concurrency, not algorithmic tricks.
Do I need C++ for the HRT interview?+
For core engineering roles, effectively yes. HRT's questions are framed in C++ and probe its memory model, RAII, smart pointers, atomics, and the standard library internals. Comfort with these is expected.
What makes the copy-on-write string question hard?+
Getting the reference counting exactly right across the constructor, copy constructor, and assignment, then realizing that a non-const accessor must detach a shared buffer before returning a mutable reference, and that making it thread-safe needs more than an atomic refcount.
How is the HRT coding round scored?+
On correct shared-buffer reference counting, copy-on-write only when shared, const versus non-const access semantics, no leaks or double frees, and an honest account of what thread safety actually requires.
Where can I find real Hudson River Trading interview questions?+
Pichup maintains an HRT question bank compiled from people familiar with the process, with the C++ systems questions, their follow-ups, and rubrics. The question in this guide is one of them.