C++

Should I pass an stdfunction by const-reference

27 September 2026 · 6 min read

Should I pass an stdfunction by const-reference

When designing C++ APIs, a common dilemma arises: how should callable objects, specifically instances of std::function, be passed to functions? This question, “should I pass an std::function by const-reference?”, delves into crucial aspects of performance, semantics, and overall code design. While passing by value might seem straightforward, and a non-const reference could imply modification, the const std::function& approach offers a balanced solution for many scenarios. Understanding the underlying mechanisms of std::function, such as type erasure and potential heap allocations, is key to making an informed decision that optimizes both runtime efficiency and code clarity.

Understanding std::function and its Characteristics

std::function is a versatile polymorphic function wrapper introduced in C++11. It can store, copy, and invoke any callable target – be it a regular function pointer, a lambda expression, a function object, or even a member function pointer – that matches its signature. This flexibility comes from a technique known as type erasure. Instead of the function’s type being part of the std::function’s template arguments, the actual callable is “erased” behind a common interface, allowing different callable types to be stored and called uniformly.

However, this type erasure isn’t without its costs. One significant characteristic of std::function is its potential for dynamic memory allocation. While it often employs a small object optimization (SOO) to store smaller callables directly within its internal buffer, larger callables or those that cannot fit may trigger a heap allocation. This means that copying an std::function might not always be a trivial operation; it could involve copying the wrapped callable and potentially performing a new memory allocation, leading to performance overheads that are not present when copying simple types like integers or pointers. This understanding is crucial when debating whether to pass an std::function by const-reference or by value.

The overheads associated with std::function extend beyond just copying. Invoking the wrapped callable through std::function also incurs a slight runtime cost due to dynamic dispatch, similar to calling a virtual function. While typically minimal for most applications, in performance-critical loops or high-frequency callbacks, these accumulated costs can become a consideration. For a deeper dive into std::function, cppreference.com provides comprehensive documentation on its behavior and capabilities.

Pass-by-Value vs. Pass-by-Reference: General Principles for Callables

In C++, the choice between passing arguments by value, by const-reference, or by non-const reference is fundamental and impacts both performance and semantics. For most primitive types like int or bool, pass-by-value is perfectly fine due to their small size and cheap copy semantics. When dealing with larger, more complex objects, however, copying can be expensive, leading developers to opt for references to avoid unnecessary copies and improve efficiency.

For callable objects, including std::function instances, these general principles still apply but are layered with additional nuances. If a function needs to take ownership of a callable, or if it needs to modify the callable internally (which is rare for std::function itself, but possible for its internal state if the callable is mutable), pass-by-value can be appropriate, especially when combined with move semantics. Moving an std::function can be significantly cheaper than copying it, as it transfers ownership of any dynamically allocated memory without reallocating.

Conversely, if a function only needs to invoke the callable and does not intend to modify it or take ownership, passing by reference is generally preferred. A non-const reference (std::function&) indicates that the function might modify the callable itself, which is rarely the semantic intent when simply using a callable. Therefore, for most cases where a function just needs to execute the provided behavior, a const-reference is the more semantically correct and efficient choice, preventing accidental modification and avoiding costly copies.

The const std::function& Conundrum: Nuances and Best Practices

When considering how to pass an std::function instance to a function, the primary concern revolves around avoiding unnecessary copies and ensuring correct semantics. For parameters that represent a callback or an operation to be performed, and where the function does not need to take ownership of the callable or modify its state, passing std::function by const reference (const std::function&) is generally the most performant and semantically correct choice. This approach avoids the potential overhead of copying the std::function object, which could involve heap allocations if the wrapped callable is large or doesn’t fit within the small object optimization buffer. By using a const reference, you clearly communicate that the function will only use the callable for invocation, not for modification or storage beyond its scope.

While const std::function& is often the default recommendation, there are specific scenarios where other passing mechanisms are more suitable. If your function needs to store the std::function object internally (e.g., in a class member Question & Answer :

Let’s say I have a function which takes an std::function:

void callFunction(std::function<void()> x) { x(); } 

Should I pass x by const-reference instead?:

void callFunction(const std::function<void()>& x) { x(); } 

Does the answer to this question change depending on what the function does with it? For example, if it is a member function or constructor which stores or initializes the std::function into a data member.

If you want performance, pass by value if you are storing it.

Suppose you have a function called “run this in the UI thread”.

std::future<void> run_in_ui_thread( std::function<void()> ) 

which runs some code in the “ui” thread, then signals the future when done. (Useful in UI frameworks where the UI thread is where you are supposed to mess with UI elements)

We have two signatures we are considering:

std::future<void> run_in_ui_thread( std::function<void()> ) // (A) std::future<void> run_in_ui_thread( std::function<void()> const& ) // (B) 

Now, we are likely to use these as follows:

run_in_ui_thread( [=]{ // code goes here } ).wait(); 

which will create an anonymous closure (a lambda), construct a std::function out of it, pass it to the run_in_ui_thread function, then wait for it to finish running in the main thread.

In case (A), the std::function is directly constructed from our lambda, which is then used within the run_in_ui_thread. The lambda is moved into the std::function, so any movable state is efficiently carried into it.

In the second case, a temporary std::function is created, the lambda is moved into it, then that temporary std::function is used by reference within the run_in_ui_thread.

So far, so good – the two of them perform identically. Except the run_in_ui_thread is going to make a copy of its function argument to send to the ui thread to execute! (it will return before it is done with it, so it cannot just use a reference to it). For case (A), we simply move the std::function into its long-term storage. In case (B), we are forced to copy the std::function.

That store makes passing by value more optimal. If there is any possibility you are storing a copy of the std::function, pass by value. Otherwise, either way is roughly equivalent: the only downside to by-value is if you are taking the same bulky std::function and having one sub method after another use it. Barring that, a move will be as efficient as a const&.

Now, there are some other differences between the two that mostly kick in if we have persistent state within the std::function.

Assume that the std::function stores some object with a operator() const, but it also has some mutable data members which it modifies (how rude!).

In the std::function<> const& case, the mutable data members modified will propagate out of the function call. In the std::function<> case, they won’t.

This is a relatively strange corner case.

You want to treat std::function like you would any other possibly heavy-weight, cheaply movable type. Moving is cheap, copying can be expensive.