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 1: Reading hot code

std::function, std::any and the cost of type erasure

Lesson · 9 min

The question

The feed handler stores its callback as std::function<void(const Quote&)>. Is that a problem on the hot path?

What type erasure does

A lambda has a unique type that only the compiler can name. To store "something callable with a Quote" in a member, a container or a map, you need one type that can hold any of them. That is what std::function does: it erases the callable's real type and keeps two things, the callable's state and a way to call it.

class FeedHandler {
    std::function<void(const Quote&)> on_quote_;
public:
    template <class F> void subscribe(F f) { on_quote_ = std::move(f); }
    void handle(const Quote& q) { on_quote_(q); }
};

std::any does the same for values: it holds an object of any copyable type and gives it back through std::any_cast with a run-time type check.

Where the cost comes from

newstd::functionsmall lambda stored insidestd::functionlarge capture: pointerheap blockthe captured statecall through a pointerno inlining
Type erasure hides the callable behind a fixed-size object. A small callable fits in the object; a large one is moved to the heap when the function is built. Either way, every call goes through a pointer.

Storage. The std::function object has a fixed size, 32 bytes in libstdc++ and libc++. It keeps small callables, such as a lambda capturing one or two pointers, inside itself. A callable with larger captures is moved to a heap allocation when the function is constructed or assigned. That is a hidden allocation of the kind in the earlier lesson, and it happens every time a new callable is stored.

Calls. Every call goes through a function pointer that knows how to invoke the stored type. So each call is an indirect call, and the lambda body cannot be inlined into the caller, even when the compiler could see it a line earlier.

Copies. Copying a std::function copies the callable, which may allocate again.

Storing the callback once at start-up and calling it many times is the benign case: one allocation at subscription, an indirect call per quote. Building a new std::function per event, for example by passing a lambda to a function that takes std::function by value, allocates on the hot path.

std::any has the same pattern: a small-object buffer, a heap allocation beyond it, and a type check on every access.

Alternatives

  • Templates. Make the handler a template on the callback type. The call is direct and inlinable, at the cost of not being able to store different callbacks in one container.
template <class OnQuote>
class FeedHandler {
    OnQuote on_quote_;
public:
    explicit FeedHandler(OnQuote f) : on_quote_(std::move(f)) {}
    void handle(const Quote& q) { on_quote_(q); }   // direct call, can be inlined
};
  • A non-owning callable reference. A function_ref (in C++26 as std::function_ref, and long available in libraries) is two pointers: the object and a function to call it with. It never allocates, which makes it right for parameters where the callable outlives the call.
  • Fixed-capacity function objects. Many trading codebases have an inplace_function with a compile-time buffer size and a compile error, rather than an allocation, when the callable does not fit. C++26 adds std::copyable_function and C++23 std::move_only_function, which still may allocate.
  • Plain function pointers plus a context pointer, the C pattern, which is what all of the above compile down to.

The interview point is the same as for virtual functions: erasure is the right tool for flexibility at the edges, and it has a known price that you should not pay per event.

Check yourself

Question 1 of 3

When does constructing a std::function from a lambda allocate?

Question 2 of 3

Why can't the compiler inline the body of a lambda called through a std::function member?

Question 3 of 3

A function takes std::function<void()> cb by value and is called on every message with a new lambda capturing a 40-byte struct. What is the main cost?

In the interview

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

Is std::function slow?

Calling it is an indirect call that cannot be inlined, which is a modest cost. Constructing or copying one can allocate when the callable is larger than its internal buffer. Storing a callback once and calling it many times is acceptable on most paths; creating std::functions per event is not.

What would you use instead of std::function for a hot callback?

A template parameter on the class that calls it, so the call is direct and inlinable. Where I need to accept any callable as a parameter without owning it, a function_ref that is just two pointers. Where I need ownership without allocation, a fixed-capacity function type that fails to compile if the callable is too large.

What is type erasure?

Hiding a concrete type behind a fixed interface so that different types can be stored and used the same way. std::function erases callables, std::any erases values. Internally it is a buffer or heap pointer for the object plus a table of operations for it, which is the same mechanism as a vtable built by the library instead of the compiler.