Skip to content
Work Practice Products Coding interviews Machine learning coding Quant developer HFT course Blog Method Why me About Enquire Book a call

Quant developer HFT course · Module 2: Safe C++

RAII: tying every resource to an object

Lesson · 9 min

The question

Explain RAII. Then tell me what is wrong with this function that takes a mutex, writes to a socket and releases the mutex.

The function

void send_order(Gateway& gw, const Order& o) {
    gw.mutex.lock();
    if (!gw.connected) return;          // returns with the mutex held
    gw.socket.write(encode(o));         // encode can throw: mutex held forever
    gw.mutex.unlock();
}

Two exits skip the unlock. The early return leaves the mutex locked, and the next thread that calls send_order waits for ever. If encode throws, for example on an allocation failure or an invalid order, the exception leaves the function without reaching unlock. Every manual release has this problem: each new exit path is another chance to forget it.

The idea

RAII, resource acquisition is initialisation, ties a resource to the lifetime of an object. The constructor acquires it, the destructor releases it. C++ guarantees that when control leaves a scope, by falling off the end, by return, by break, or by an exception, the destructor of every fully constructed local object runs. So the release cannot be skipped.

constructoracquireusenormal returnexception throwndestructorrelease
Every way out of a scope runs the destructor of every fully constructed local. Tie the resource to an object and the release cannot be skipped.
void send_order(Gateway& gw, const Order& o) {
    std::lock_guard<std::mutex> lock(gw.mutex);   // locks here
    if (!gw.connected) return;                    // unlocks here
    gw.socket.write(encode(o));                   // or here, if encode throws
}                                                 // or here

The name is awkward; the idea is that a resource is only ever held by an object whose destructor gives it back. The standard library is built on it: std::lock_guard and std::unique_lock for mutexes, std::unique_ptr and std::shared_ptr for heap memory, std::vector and std::string for buffers, std::fstream for files, std::jthread for a thread that joins itself.

Resources beyond memory

In trading systems the resources that matter are often not memory: a socket, a file descriptor for a shared-memory segment, a registration with an exchange session, a reserved slot in a ring buffer, a CPU core claimed for a thread. Each needs a release that must happen exactly once. Wrapping each in a small class with a destructor gives the same guarantee for all of them.

class Fd {
    int fd_ = -1;
public:
    explicit Fd(int fd) : fd_(fd) {}
    ~Fd() { if (fd_ >= 0) ::close(fd_); }
    Fd(const Fd&) = delete;                         // two owners would close twice
    Fd& operator=(const Fd&) = delete;
    Fd(Fd&& o) noexcept : fd_(std::exchange(o.fd_, -1)) {}
    Fd& operator=(Fd&& o) noexcept {
        if (this != &o) { if (fd_ >= 0) ::close(fd_); fd_ = std::exchange(o.fd_, -1); }
        return *this;
    }
    int get() const { return fd_; }
};

Writing a wrapper correctly

The class above shows the decisions every RAII type has to make:

  • Copying. If copying would create two owners of one resource, delete the copy operations. A file descriptor closed twice may close a descriptor that has since been reused for something else.
  • Moving. Allow it, transferring ownership and leaving the source empty, so that the wrapper can be returned from functions and stored in containers. Mark the moves noexcept so containers use them when they grow (lesson 5).
  • Destructors must not throw. A destructor that throws while another exception is propagating terminates the program. Destructors are implicitly noexcept for this reason.
  • The rule of zero and the rule of five. If a class only holds members that already manage themselves, write none of the five special functions and let the compiler generate them. If it manages a resource directly, write or delete all five: destructor, copy constructor, copy assignment, move constructor, move assignment.

RAII and latency

RAII has no run-time cost beyond the release itself: the destructor call is inlined at each exit. It does make one cost easy to miss. A destructor that frees a large structure, joins a thread, or flushes a file runs at the closing brace, which in a hot function is an invisible line of work. Look at what objects are destroyed at the end of a hot scope, not only what is written inside it.

Check yourself

Question 1 of 3

Why does std::lock_guard release the mutex when an exception is thrown inside its scope?

Question 2 of 3

A class owns a raw file descriptor. Which set of special members is right?

Question 3 of 3

What happens if a destructor throws while the stack is being unwound by another exception?

In the interview

Say your own answer out loud first, then open the one we would give.

What is RAII?

Tying the lifetime of a resource to the lifetime of an object: acquire in the constructor, release in the destructor. Because C++ runs destructors of locals on every exit from a scope, including exceptions, the release cannot be forgotten. lock_guard, unique_ptr, vector and fstream all work this way.

Why is RAII important for exception safety?

Every function that can throw has hidden exit paths. With manual cleanup each one is a leak or a held lock. With RAII, unwinding releases everything automatically, so code is correct on the exception path without any try blocks.

What is the rule of five, and the rule of zero?

If a class manages a resource directly, it needs to define or delete all five special members: destructor, copy constructor, copy assignment, move constructor, move assignment. The rule of zero says to avoid that where possible by composing classes from members that already manage themselves, so the compiler-generated members are correct.

Is there a performance cost to RAII?

The mechanism itself is free; the destructor call is inlined where the scope ends. The release is whatever it was going to be. The thing to watch is expensive destructors at the end of hot scopes, such as freeing large structures, which RAII makes easy to overlook.