Templates[temp]
std::function in C++: what it costs, what to use instead
- Section
- Templates [temp]
- Standard
- C++17
- Compilers
- MSVC 19.44, Clang 19.1 (MSVC STL), GCC 16.2, Clang 23.1 (libc++)
- Published
- Length
- 14 min read
In this answer10 clauses
- 1What std::function holds
- 2std::function vs a function pointer
- 3Calling an empty std::function
- 4Does std::function allocate? Size and allocations per library
- 5A std::function owns a copy of its callable
- 6Move-only callables and const: std::move_only_function
- 7Taking a callable as a parameter
- 8std::bind or a lambda
- 9std::invoke: one way to call any callable
- 10Which to use
Abstract
std:: stores a copy of any copyable callable with that signature (a function, a lambda with captures, a function object, a pointer to a member function) and usually calls it through a pointer. It may allocate, and on x86-64 it is 32 to 64 bytes, depending on the standard library. Take a template parameter when a function only calls its argument, a function pointer for a C interface, and std:: (C++23; in libstdc++ and Microsoft's STL, not libc++ 23) for a callable that cannot be copied.

std:: is a copyable object holding a copy of any copy-constructible callable with that signature: a function, a lambda with or without captures, a function object, a member function pointer. Use it to store callables whose types are not known where they are stored: a callback member, a list of handlers. The price: it is several pointers big, it may allocate, and a call usually goes through a pointer. A function that only calls its argument should take a template parameter; a function pointer holds only functions and lambdas with an empty capture list, [].
This answer is written for C++17. std::function itself is C++11 (Since When’s C++11 table has its row); C++17 gave it a deduction guide, added std::invoke, and removed the allocator constructors and assign that older references still list. C++20 added std::bind_front, C++23 std:: and std::bind_back, and C++26 adds std:: and std::function_ref.
The samples were built with MSVC 19.44, Clang 19.1 (MSVC STL), GCC 16.2 and Clang 23.1 (libc++): the first two on Windows x64, sharing Microsoft’s standard library; GCC with libstdc++ and Clang 23.1 with libc++ on Compiler Explorer, Linux x86-64. MSVC and Clang 19.1 printed the same everywhere, so the tables have one column per library. Nothing was timed: the samples count bytes and allocations, and the counts were the same with and without optimization.
What changed: arguments are bound to a function with a lambda.std::bind and placeholders
since C++14
What std::function holds
One std:: takes a function, then a lambda that captures an offset, then a function object; a member function goes in a std::function whose first parameter is the object:
double to_fahrenheit(double celsius) { return celsius * 1.8 + 32.0; }
struct Scale { // a function object: a factor and a call operator
double factor;
double operator()(double v) const { return v * factor; }
};
// ...
std::function<double(double)> convert = to_fahrenheit;
std::printf("%.1f\n", convert(20.0)); // 68.0
double offset = -0.5;
convert = [offset](double v) { return v + offset; };
std::printf("%.1f\n", convert(20.0)); // 19.5
convert = Scale{1000.0};
std::printf("%.1f\n", convert(20.0)); // 20000.0
std::function<double(const Sensor&)> read = &Sensor::value;
Sensor probe(21.5);
std::printf("%.1f\n", read(probe)); // 21.5
std::function fahrenheit = to_fahrenheit; // C++17: deduced
std::function shifted = [offset](double v) { return v + offset; };— end example]
All four printed what the comments say, and both deductions gave std::. What goes in must be copy-constructible and callable with the signature’s arguments ([func.wrap.func]).
Storing is the job it exists for: callables of different types in one member or one container. A Pipeline of conversion steps:
class Pipeline {
public:
void add(std::function<double(double)> step) {
steps_.push_back(std::move(step));
}
double run(double value) const {
for (const auto& step : steps_) value = step(value);
return value;
}
private:
std::vector<std::function<double(double)>> steps_;
};
// ...
double offset = -0.5;
Pipeline pipeline;
pipeline.add(Scale{0.1}); // raw tenths of a degree to degrees
pipeline.add([offset](double v) { return v + offset; });
pipeline.add(to_fahrenheit);
std::printf("%.2f\n", pipeline.run(215.0)); // 69.80— end example]
It printed 69.80 on all four. Two std::function objects cannot be compared with each other, only with nullptr, so a list of handlers that must remove one keeps an id or a token beside each.
std::function vs a function pointer
A function pointer holds the address of a function and nothing else: 8 bytes on both platforms tested, no allocation, and the type a C API or a table of plain functions wants. A using alias makes it readable:
using Conversion = double (*)(double); // a pointer to such a function
// ...
double (*convert)(double) = to_fahrenheit; // & is optional
std::printf("%.2f\n", convert(20.0)); // 68.00
Conversion table[] = {to_fahrenheit, to_kelvin};
for (Conversion c : table) std::printf("%.2f\n", c(20.0));
convert = [](double v) { return v * 1000.0; }; // no captures
std::printf("%.2f\n", convert(20.0)); // 20000.00
Conversion none = nullptr;
if (!none) std::puts("no function"); // test it; never call it— end example]
The table printed 68.00 and 293.15 on all four. A lambda with an empty capture list converts to a function pointer; one that captures does not:
double (*convert)(double) =
[offset](double v) { return v + offset; }; // error— end example]
All four rejected it, and none named the capture. GCC’s line is the shortest: cannot convert 'main()::. The reason: a captured value needs somewhere to live, and a function pointer points only at code. Only a lambda with an empty capture list has a conversion to a function pointer, which is what MSVC’s note means by No user-defined-conversion operator available; a [=] or [&] lambda that uses nothing has none either, on all four.
Calling an empty std::function
A default-constructed std::function is empty: it converts to false, and calling it throws std:: ([func.wrap.func.inv]). One made from a null function pointer is empty too:
std::function<double(double)> convert; // holds nothing
if (!convert) std::puts("empty");
try {
convert(20.0);
} catch (const std::bad_function_call& e) {
std::printf("caught: %s\n", e.what());
}
double (*none)(double) = nullptr;
std::function<double(double)> wrapped = none; // empty, too
std::printf("wrapped holds a target: %s\n", wrapped ? "yes" : "no");
try {
wrapped(20.0); // throws: it never calls the null pointer
} catch (const std::bad_function_call&) {
std::puts("caught again");
}— end example]
All four printed empty, a caught: line, wrapped holds a target: no and caught again. Each library words what() its own way, so catch the type, not the message. Calling a null function pointer directly is undefined behavior; the std::function built from one threw instead.
Does std::function allocate? Size and allocations per library
It may, and when depends on the library. The standard says only that implementations “should avoid the use of dynamically allocated memory for small callable objects”, and promises only that building one from a function pointer or a std::, or copying one that holds either, throws nothing ([func.wrap.func.con]). Each library keeps a callable inside the std::function object only when it is small enough and passes a second test of its own:
| On x86-64 | Microsoft’s STL | libstdc++ | libc++ |
|---|---|---|---|
sizeof(std:: |
64 | 32 | 48 |
sizeof a function pointer |
8 | 8 | 8 |
| Callable kept inside, up to | 48 bytes | 16 bytes | 24 bytes |
| And only if the callable is | movable without throwing | trivially copyable | copyable without throwing |
Any other callable lives on the heap, and the object points to it:

A replaced global operator new counted the allocations, reset before constructing a std:: from each callable and again before copying it:
int allocations = 0; // every call to operator new
void* operator new(std::size_t size) {
++allocations;
if (void* p = std::malloc(size ? size : 1)) return p;
throw std::bad_alloc{};
}
// ...
template <class F>
void measure(const char* what, F callable) {
allocations = 0;
std::function<void()> stored = std::move(callable);
int constructed = allocations;
allocations = 0;
std::function<void()> copy = stored;
int copied = allocations;
// ...— end example]
Allocations made by the construction, in release builds; copying the std::function made the same count again, in every row, on every library:
| Callable (bytes) | Microsoft’s STL | libstdc++ | libc++ |
|---|---|---|---|
| Function pointer (8) | 0 | 0 | 0 |
| Lambda, 2 pointers (16) | 0 | 0 | 0 |
| Lambda, 3 pointers (24) | 0 | 1 | 0 |
| Lambda, 4 pointers (32) | 0 | 1 | 1 |
Lambda, short std::string (32; 24 on libc++) |
0 | 1 | 1 |
Lambda, std::shared_ptr (16) |
0 | 1 | 0 |
| Lambda, 64-byte array (64) | 1 | 1 | 1 |
std::ref(lambda) (8) |
0 | 0 | 0 |
Size is not the whole rule. The std::shared_ptr lambda is 16 bytes and allocated on libstdc++ alone, because it is not trivially copyable. The short std::string lambda allocated on libstdc++, and on libc++ too, where it is 24 bytes and fits: a string’s copy is not noexcept. Microsoft’s STL kept both inside, but with its debug library (/MDd) the string lambda is 40 bytes and allocated on construction and again on copy.
Wherever the callable sits, a call goes through a pointer the object keeps unless the optimizer can see what was stored: at -O2, GCC 16.2 and Clang 23.1 on libstdc++ inlined a std::function built and called in the same function, but not one received as a const std::function& parameter. A template parameter’s call is a direct call the compiler can inline. The Core Guidelines’ T.49 asks you to avoid type erasure where possible, since it “incurs an extra level of indirection”, and names std::function as a case where it is appropriate.
A std::function owns a copy of its callable
Constructing a std::function copies or moves the callable into it, and copying the std::function copies the callable again, state and all:
auto next_id = [id = 0]() mutable { return ++id; };
std::function<int()> first = next_id; // a copy of the lambda
first();
first();
std::function<int()> second = first; // a copy of that copy
std::printf("first %d\n", first()); // 3
std::printf("second %d\n", second()); // 3: its own counter
std::printf("next_id %d\n", next_id()); // 1: never called before
auto counter = [n = 0]() mutable { return ++n; };
std::function<int()> a = std::ref(counter); // refers to counter
std::function<int()> b = a; // still refers to counter
a();
b();
std::printf("counter %d\n", counter()); // 3: one shared counter— end example]
All four printed what the comments say. Through std::ref, every copy shares one lambda, which must then outlive them all. The opposite trap is a lambda that captures a local by reference and is stored past that local’s life: the reference dangles. The Core Guidelines’ F.53 says to avoid capturing by reference in lambdas that will be used non-locally, including stored on the heap.
Move-only callables and const: std::move_only_function
std::function requires a copy-constructible target, so a lambda that owns a std::unique_ptr does not go in:
auto factor = std::make_unique<double>(1.8);
std::function<double(double)> step = // error: cannot be copied
[f = std::move(factor)](double v) { return v * *f; };— end example]
GCC names the rule: static assertion failed: std::. Microsoft’s STL says the same in its own static_assert; libc++ reports only a call to the lambda’s deleted copy constructor. C++23’s std:: holds it ([func.wrap.move]), and its signature can say const:
auto scale = std::make_unique<Scale>(Scale{1.8});
std::move_only_function<double(double)> step = // C++23
[s = std::move(scale)](double v) { return (*s)(v); };
std::printf("%.1f\n", step(20.0)); // 36.0
auto owner = std::move(step); // moved; a copy does not compile
std::printf("%.1f\n", owner(10.0)); // 18.0
std::move_only_function<int()> next_id = [id = 0]() mutable {
return ++id;
};
next_id();
std::printf("next_id %d\n", next_id()); // 2
const std::move_only_function<double(double) const> convert =
Scale{1.8};
std::printf("%.1f\n", convert(20.0)); // 36.0: const, callable— end example]
MSVC, Clang 19.1 and GCC printed what the comments say, and a lambda capturing a unique_ptr went in without an allocation on Microsoft’s STL and libstdc++. Clang 23.1 stopped at the first use: libc++ 23.1 has no std::. libstdc++ has it since GCC 12.1 and Microsoft’s STL since Visual Studio 2022 17.2 (Since When’s C++23 table).
The const closes a hole in std::function, whose call operator is const even when the target’s is not. A const std::function runs a mutable lambda and changes it:
const std::function<int()> next_id = [id = 0]() mutable {
return ++id;
};
std::printf("%d\n", next_id()); // 1
std::printf("%d\n", next_id()); // 2: a const object changed— end example]
The same mutable lambda, three ways:
| Case | Compiles | What happened |
|---|---|---|
Calling a const std:: that holds it |
yes, as C++17 and C++23 | printed 1, then 2 |
Calling a const std:: that holds it |
no | its call operator is not const |
Building a std:: from it |
no | a conversion error that does not say why |
Clang’s note on the second case names the cause: 'this' argument has type 'const std::.
Two more differences: calling an empty std:: is a precondition violation, not an exception, and the signature can also say noexcept. C++26’s std:: brings the same signatures to a copyable wrapper; it is in libstdc++ only, since GCC 16.1 (Since When’s C++26 table).
Taking a callable as a parameter
A function that only calls its argument should take a template parameter, or auto since C++20: the call is direct and nothing is allocated. The same apply_all four ways:
namespace by_template {
template <class F>
void apply_all(std::vector<double>& readings, F convert) {
for (double& r : readings) r = convert(r);
}
} // namespace by_template
namespace by_auto {
void apply_all(std::vector<double>& readings, auto convert) { // C++20
for (double& r : readings) r = convert(r);
}
} // namespace by_auto
namespace by_pointer {
void apply_all(std::vector<double>& readings,
double (*convert)(double)) {
for (double& r : readings) r = convert(r);
}
} // namespace by_pointer
namespace by_function {
void apply_all(std::vector<double>& readings,
const std::function<double(double)>& convert) {
for (double& r : readings) r = convert(r);
}
} // namespace by_function— end example]
Allocations per call, with [offset] capturing a double and [cal] copying a 32-byte Calibration:
| Parameter, argument | Microsoft’s STL | libstdc++ | libc++ |
|---|---|---|---|
Template or auto, [offset] |
0 | 0 | 0 |
Template, [cal] |
0 | 0 | 0 |
std::function, [offset] |
0 | 0 | 0 |
std::function, [cal] |
0 | 1 | 1 |
A std::function parameter builds a new std::function from the lambda at every call: ten calls with [cal] made ten allocations on libstdc++ and libc++. The pointer version took to_fahrenheit without allocating and rejected [offset] on all four. Writing the template is covered in C++ templates, and auto parameters in generic lambdas and function objects.
C++26’s std::function_ref is the parameter type made for this: it refers to the callable instead of owning a copy, and does not allocate ([func.wrap.ref]):
void apply_all(std::vector<double>& readings,
std::function_ref<double(double)> convert) { // C++26
for (double& r : readings) r = convert(r);
}— end example]
It is in libstdc++ only, since GCC 16.1; GCC 16.2 built it and counted no allocation for the [cal] lambda. Microsoft’s STL and libc++ 23.1 do not have it (MSVC: C2039: 'function_ref': is not a member of 'std'); libc++’s C++26 status page lists it for LLVM 24. A function_ref must not outlive what it refers to: made from a temporary lambda, it dangles once the full expression ends, so keep it to parameters.
std::bind or a lambda
Use a lambda: std:: and [](double v) { return scale(v, 1.8); } compute the same result, and the lambda says what it does. std::bind_front (C++20) fixes leading arguments without placeholders:
using std::placeholders::_1;
auto by_bind = std::bind(scale, _1, 1.8);
auto by_lambda = [](double v) { return scale(v, 1.8); };
std::printf("%.1f %.1f\n", by_bind(20.0), by_lambda(20.0)); // 36.0
Sensor probe(21.5);
auto read = std::bind_front(&Sensor::scaled, &probe); // C++20
std::printf("%.1f\n", read(2.0)); // 43.0
double factor = 1.8;
auto kept = std::bind(scale, _1, factor); // a copy of factor
auto shared = std::bind(scale, _1, std::ref(factor));
factor = 2.0;
std::printf("%.1f %.1f\n", kept(10.0), shared(10.0)); // 18.0 20.0— end example]
All four printed 36.0 36.0, 43.0 and 18.0 20.0: std::bind stores a copy of each bound argument, so the change to factor reached only the std::ref version ([func.bind.bind]). The bind result also accepts and ignores extra arguments: by_bind(20.0, 99, "ignored") printed 36.0 on all four, and the lambda cannot be called with two. std::bind_back (C++23) fixes the last arguments; all four rejected it as C++20 and built it as C++23.
clang-tidy’s modernize-avoid-bind flags each std::bind (prefer a lambda to std::), and version 19.1.5 offered a lambda with a forwarding auto&& parameter as the fix; how to run clang-tidy has the setup. Abseil’s Tip 108 gives the same advice: avoid std::bind, use a lambda or std::bind_front.
std::invoke: one way to call any callable
std:: (C++17) calls a function, a lambda, a member function on an object or through a pointer to one, or reads a data member, all with one syntax ([func.invoke]). It is what generic code writes instead of f(args...):
auto add_offset = [offset](double v) { return v + offset; };
double a = std::invoke(to_fahrenheit, 20.0); // 68.0
double b = std::invoke(add_offset, 20.0); // 19.5
double c = std::invoke(&Sensor::value, probe); // 21.5
double d = std::invoke(&Sensor::value, &probe); // 21.5
double e = std::invoke(&Reading::celsius, reading); // 19.0— end example]
All four printed 68.0 19.5 21.5 21.5 19.0. C++23 adds std::invoke_r<R>, which converts the result to R.
Which to use
| Job | Use | Since | What it costs |
|---|---|---|---|
| Call it now, inside this function | A template parameter, or auto |
before C++17; auto parameters C++20 |
No allocation; a direct call |
| A C API, a table of plain functions | A function pointer | before C++17 | 8 bytes; lambdas only with an empty capture list, [] |
| Store any copyable callable | std::function |
C++11 | 32 to 64 bytes, may allocate, usually an indirect call |
Store a move-only callable, or make the call const |
std:: |
C++23; not in libc++ 23.1 | Like std::function, never copied |
| A parameter you only call, without a template | std::function_ref |
C++26; libstdc++ only, since GCC 16.1 | No allocation; must not outlive the callable |
| Bind arguments | A lambda, or std::bind_front |
C++11; bind_front C++20 |
The captures |
| Choose behavior at run time across a class hierarchy | A virtual function | before C++17 | A call through the object’s vtable |
More answers on templates and generic code are in Templates.
Questions
Is std::function the same as a function pointer?
No. A function pointer holds the address of a function and nothing else: 8 bytes on x86-64, no allocation, and it accepts only functions and lambdas with an empty capture list. A std::function holds a copy of any callable with its signature, captures included, is 32 to 64 bytes on x86-64 and may allocate. Called empty, it throws std::, where calling a null function pointer is undefined behavior.
Does std::function allocate memory?
It may: the standard only says implementations should avoid allocating for small callables. On x86-64, in the releases tested (MSVC 19.44, GCC 16.2, Clang 23.1 with libc++), Microsoft’s STL kept a callable inside the object up to 48 bytes if its move cannot throw, libstdc++ up to 16 bytes if it is trivially copyable, and libc++ (its default ABI) up to 24 bytes if its copy cannot throw. Anything else was allocated on the heap, and copying that std::function allocated again.
Can std::function store a lambda with captures or a member function?
Yes, both. A lambda with captures is copied into the std::function, captures and all, provided it is copy-constructible. A member function goes in a std::function whose first parameter is the object: std::function<double(const Sensor&)> read = &Sensor::value; is then called as read(probe).
Why can’t std::function hold a lambda that captures a unique_ptr?
The standard requires the target of a std::function to be copy-constructible, and a lambda that owns a std::unique_ptr is not. GCC 16.2 says so: static assertion failed: std::function target must be copy-constructible. C++23’s std:: holds it; libstdc++ has it since GCC 12.1 and Microsoft’s STL since Visual Studio 2022 17.2, while libc++ 23.1 does not have it.
Should I use std::bind or a lambda?
A lambda: [](double v) { return scale(v, 1.8); } says what std::bind(scale, _1, 1.8) hides behind a placeholder, and clang-tidy’s modernize-avoid-bind check flags each std::bind and offers a lambda in its place, except where one placeholder is used twice. To fix only the first arguments, C++20’s std::bind_front needs no placeholders; C++23’s std::bind_back fixes the last ones. All three copy their bound arguments unless you pass std::ref.
Is std::function slow?
No timing is given here, only what it does. A call usually goes through a pointer, where a template parameter’s call is direct and can be inlined, constructing or copying one may allocate, and the object is 32 to 64 bytes on x86-64. A function that only calls its argument avoids all three by taking a template parameter.
Sources
- std::functioncppreference.com
- [func.wrap.func] Class template functionC++ standard working draft
- [func.wrap.move] Move-only wrapperC++ standard working draft
- [func.wrap.ref] Non-owning wrapperC++ standard working draft
- [func.bind.bind] Function template bindC++ standard working draft
- T.49: Where possible, avoid type-erasureC++ Core Guidelines
- modernize-avoid-bindclang-tidy documentation
- Tip of the Week #108: Avoid std::bindAbseil