Function Pointers and Callables
Raw function pointers, member function pointers, std::function, std::invoke, and zero-cost template callbacks in modern C++.
Callablesince C++98A callable is any entity that can be invoked with the function-call syntax: free functions, function pointers, member function pointers, objects with operator(), and — since C++11 — lambdas and std::function instances.
Overview
The callable taxonomy in C++ has grown across standards:
- C++98: free function pointers, member function pointers, functors (
operator()) - C++11: lambdas,
std::function,std::bind - C++17:
std::invoke,noexceptbecomes part of the function type - C++20: concepts (
std::invocable,std::regular_invocable),std::bind_front - C++23:
std::move_only_function,std::copyable_function
These aren't interchangeable. The right choice is a design decision with real performance and ergonomic consequences; the sections below cover each form and when to reach for it.
Syntax
Free Function Pointers
// Declaration: return-type (*name)(params)
int (*fp)(int, int);
// Assignment — & is optional; functions decay to pointers implicitly
int add(int a, int b) { return a + b; }
fp = add; // implicit decay
fp = &add; // explicit — identical result
// Invocation — both forms are legal
int r = fp(3, 4); // 7
int s = (*fp)(3, 4); // 7 — explicit dereference, rarely used
// Type alias: prefer 'using' over 'typedef' (C++11)
using BinaryOp = int (*)(int, int); // C++11 — name is clearly to the right
typedef int (*BinaryOpOld)(int, int); // C++98 — name buried in the middle
// noexcept is part of the function type since C++17
using NothrowOp = int (*)(int, int) noexcept; // C++17A noexcept function pointer (NothrowOp) is a distinct type from a throwing one. A noexcept function implicitly converts to the throwing variant, but not the reverse — passing a potentially-throwing function where noexcept is required is a compile error since C++17.
Member Function Pointers
struct Timer {
std::string label;
void start() { /* ... */ }
void reset(int ms) { /* ... */ }
int elapsed() const { return 0; }
};
// Syntax: return-type (Class::*name)(params) cv-qualifiers
void (Timer::*start_ptr)() = &Timer::start;
void (Timer::*reset_ptr)(int) = &Timer::reset;
int (Timer::*elapsed_ptr)() const = &Timer::elapsed;
// Invocation requires an object — .* for value, ->* for pointer
Timer t{"perf"};
(t.*start_ptr)();
(t.*reset_ptr)(500);
Timer* pt = &t;
(pt->*start_ptr)();
// Data member pointer (reads or writes the member)
std::string Timer::*label_ptr = &Timer::label;
t.*label_ptr = "render"; // writeMember function pointers have the most complex declaration syntax in the language. They see legitimate use in generic dispatch infrastructure (e.g., signal/slot systems), but at call sites they are nearly always better expressed as lambdas.
Examples
Dispatch Table
Arrays of function pointers give O(1) dispatch without inheritance or virtual calls:
enum class Op { Add, Sub, Mul, Div };
constexpr int do_add(int a, int b) noexcept { return a + b; }
constexpr int do_sub(int a, int b) noexcept { return a - b; }
constexpr int do_mul(int a, int b) noexcept { return a * b; }
constexpr int do_div(int a, int b) noexcept { return a / b; }
using BinaryOp = int (*)(int, int) noexcept; // C++17: noexcept in type
constexpr std::array<BinaryOp, 4> dispatch{ // C++11
do_add, do_sub, do_mul, do_div
};
int evaluate(Op op, int a, int b) {
return dispatch[static_cast<int>(op)](a, b);
}The table approach beats a switch chain when the number of operations is large and the branch predictor struggles with the pattern — the indirect call replaces unpredictable conditional branches.
std::function: Type-Erased Callable Storage (C++11)
#include <functional>
// Any callable matching void(int) can be stored
std::function<void(int)> handler; // null by default
handler = [](int x) { std::print("{}\n", x); }; // lambda
handler = [](int x) { std::print("v={}\n", x); }; // replace with another
// Null check before invocation — throws std::bad_function_call if null
if (handler) handler(42);
// Member function via lambda: cleaner than std::bind
struct Sink {
void consume(int n) { total += n; }
int total = 0;
};
Sink s;
std::function<void(int)> sink_fn = [&s](int n) { s.consume(n); };
// Heterogeneous pipeline
std::vector<std::function<void()>> steps;
steps.emplace_back([] { /* init */ });
steps.emplace_back([&s] { s.consume(1); });
for (auto& step : steps) step();std::function heap-allocates closures that exceed the implementation's small-buffer-optimization (SBO) threshold — typically 16–32 bytes. It also dispatches through a vtable internally. Neither cost is free; profile before blaming it, but keep it out of tight inner loops.
std::move_only_function (C++23)
std::function requires copyable callables. Move-only captures — unique_ptr, file handles, non-copyable contexts — require the C++23 alternative:
#include <functional>
std::unique_ptr<int> resource = std::make_unique<int>(42);
// std::function would fail: the lambda is not copyable
std::move_only_function<int()> fn = // C++23
[r = std::move(resource)]() { return *r; };
int val = std::move(fn)(); // 42; fn is empty afterwardstd::move_only_function is also slightly faster than std::function because it does not need to maintain a copy-construction path in its vtable.
std::invoke for Generic Code (C++17)
std::invoke provides a single uniform call protocol across all callable categories, including member pointers:
#include <functional>
#include <chrono>
struct Widget {
int id;
void render(float scale) const { /* ... */ }
};
Widget w{7};
std::invoke(&Widget::render, w, 1.5f); // member function on value
std::invoke(&Widget::render, &w, 1.5f); // member function on pointer
std::invoke(&Widget::id, w); // member data — yields int&
// Generic wrapper that handles every callable kind uniformly
template <typename F, typename... Args>
decltype(auto) timed_call(F&& f, Args&&... args) {
auto t0 = std::chrono::steady_clock::now();
decltype(auto) result =
std::invoke(std::forward<F>(f), std::forward<Args>(args)...);
auto dt = std::chrono::steady_clock::now() - t0;
std::print("{}µs\n",
std::chrono::duration_cast<std::chrono::microseconds>(dt).count());
return result;
}Without std::invoke, a generic wrapper needs if constexpr or tag dispatch to handle member pointers separately. With it, one line covers all cases.
Zero-Cost Template Callbacks with Concepts (C++20)
#include <concepts>
#include <vector>
// C++20: constrain for readable errors, no runtime overhead
template <std::invocable<int&> F>
void transform_inplace(std::vector<int>& v, F&& f) {
for (auto& x : v) f(x);
}
// Constrain on return type using C++17 trait + C++20 requires
template <typename F>
requires std::same_as<std::invoke_result_t<F, int>, bool>
void keep_if(std::vector<int>& v, F pred) {
std::erase_if(v, [&](int x) { return !pred(x); }); // C++20
}
std::vector<int> data{1, 2, 3, 4, 5};
transform_inplace(data, [](int& x) { x *= 2; });
keep_if(data, [](int x) { return x > 4; }); // data == {6, 8, 10}The template version costs nothing at runtime — the compiler sees the concrete lambda type and inlines the body. std::function cannot do this.
Lambda-to-Function-Pointer Conversion and C Interop
Non-capturing lambdas implicitly convert to matching function pointers since C++11:
// Implicit conversion — no capture means stateless, addressable // C++11
auto fn = [](int x, int y) -> int { return x + y; };
int (*fp)(int, int) = fn; // OK
fp(3, 4); // 7
// C API interop: pass a non-capturing lambda directly as a callback
std::vector<int> v{3, 1, 4, 1, 5};
std::qsort(v.data(), v.size(), sizeof(int),
[](const void* a, const void* b) noexcept -> int {
return *static_cast<const int*>(a) - *static_cast<const int*>(b);
});A capturing lambda cannot convert to a function pointer — there is no place to store the captured state inside a bare pointer. For C APIs that pass a void* context, store state there and write a non-capturing shim that casts it back.
std::bind_front (C++20) vs Lambda
struct Logger {
void log(int level, std::string_view msg) const;
};
Logger lg;
// std::bind (C++11) — placeholder syntax is opaque
auto warn_bind = std::bind(&Logger::log, &lg, 2, std::placeholders::_1);
// Lambda — explicit, debugger-friendly, works with overloads
auto warn_lam = [&lg](std::string_view msg) { lg.log(2, msg); };
// std::bind_front (C++20) — clean for binding leading arguments
auto warn_front = std::bind_front(&Logger::log, &lg, 2); // C++20
warn_lam("disk full");
warn_front("disk full");Prefer lambdas. std::bind_front is acceptable in narrow cases (binding this + a few leading args), but a lambda is always more transparent to the compiler and the reader.
Best Practices
- Template parameter
Ffor zero-overhead callbacks: when the callable type is resolved at the call site, passF&&and call viastd::invoke. The compiler inlines; virtual dispatch is eliminated. std::functionfor type-erased interfaces: event systems, command queues, and virtual-replacement patterns need a single stable type. Size closures with SBO in mind — captures over ~24 bytes will heap-allocate on most implementations.std::move_only_function(C++23) for unique-ownership captures: any closure capturing aunique_ptr, file descriptor, or other non-copyable resource requires this overstd::function.usingovertypedeffor aliases:using BinaryOp = int(*)(int, int)is unambiguously readable; the typedef form buries the alias name mid-declaration.- Non-capturing lambdas for C API callbacks: they convert implicitly to function pointers; no hand-written wrapper needed.
- Avoid raw member function pointers at API surfaces: wrap at the call site with a lambda and expose a clean
std::functionor template interface.
Common Pitfalls
Invoking a null std::function throws std::bad_function_call. Default-constructed and nullptr-assigned instances are null; always guard with if (f) where the callable may be empty.
std::function<void()> f; // null
f(); // throws std::bad_function_callDangling reference capture in stored callables. A lambda capturing by reference and stored in std::function beyond the captured object's lifetime is silent undefined behavior — the compiler issues no warning.
std::function<void()> handler;
{
int local = 42;
handler = [&local] { std::print("{}\n", local); };
}
handler(); // UB: local is destroyedstd::bind with overloaded functions fails to compile because the address of an overloaded function is ambiguous. Use a lambda, which deduces the correct overload at the call site.
Assuming noexcept function pointers are drop-in replacements (C++17+). void (*)() and void (*)() noexcept are different types. Code that previously accepted both will produce conversion errors after enabling C++17.
std::function copy cost with large closures. std::function is CopyConstructible. Storing it in a container and copying that container copies every heap-allocated closure. Prefer std::move where possible, or use std::move_only_function (C++23) to prevent accidental copies.
Function pointer vs std::function
Raw function pointers are zero-overhead but inflexible; std::function is flexible but carries allocation and vtable cost. Choose based on whether the callable type needs to be erased at runtime or is known at compile time.
#include <functional>
// Raw pointer: one machine word, direct call, no heap
int (*fp)(int, int) = [](int a, int b) { return a + b; }; // C++11 — non-capturing
// std::function: type-erased, heap-allocates large closures
std::function<int(int, int)> fn = fp; // C++11 — stores any compatible callable
// Benchmark-level difference in a tight loop:
// fp(a, b) → single indirect call through a register
// fn(a, b) → virtual dispatch + possible heap indirectionThe critical distinction is type erasure: a raw pointer encodes exactly one calling convention, while std::function can hold any callable with the right signature — but pays for that flexibility every time it is invoked. Use raw pointers (or template parameters) in performance-sensitive paths; use std::function when you must store or pass heterogeneous callables through a stable API boundary.
Pointer to member function
Member function pointers form a distinct type hierarchy that cannot be converted to or from free-function pointers. They always require an object instance at the call site.
struct Button {
std::string label;
void click() { /* ... */ }
void hover(int x, int y) const { /* ... */ }
};
// Type: return-type (Class::*)(params) cv
void (Button::*on_click)() = &Button::click; // C++98
void (Button::*on_hover)(int, int) const = &Button::hover;
Button b{"OK"};
Button* pb = &b;
(b.*on_click)(); // call via reference — .* syntax
(pb->*on_click)(); // call via pointer — ->* syntax
(b.*on_hover)(10, 20);
// std::invoke normalises the syntax — preferred in generic code
#include <functional>
std::invoke(on_click, b); // C++17 — same as (b.*on_click)()
std::invoke(on_hover, pb, 10, 20); // C++17 — pointer variant
// Data member pointer — yields a reference, not a copy
std::string Button::*lbl_ptr = &Button::label;
b.*lbl_ptr = "Cancel"; // write through member pointerThe ->* and .* operators have lower precedence than the function-call (), so the extra parentheses around (b.*ptr) are mandatory, not stylistic. In new code, wrapping calls in lambdas or using std::invoke avoids this syntax entirely.
Function pointers in C APIs
Many C library callbacks accept a bare function pointer plus a void* userdata argument; this is the canonical pattern for passing state through a C API boundary. Non-capturing lambdas convert implicitly; capturing lambdas require a trampoline.
#include <cstdlib>
#include <cstdio>
// --- sorting with a stateless comparator ---
int cmp_desc(const void* a, const void* b) noexcept {
return *static_cast<const int*>(b) - *static_cast<const int*>(a);
}
int data[] = {3, 1, 4, 1, 5, 9};
std::qsort(data, 6, sizeof(int), cmp_desc);
// Non-capturing lambda — implicitly converts to function pointer // C++11
std::qsort(data, 6, sizeof(int),
[](const void* a, const void* b) noexcept -> int {
return *static_cast<const int*>(b) - *static_cast<const int*>(a);
});
// --- void* context pattern for stateful callbacks ---
struct Context { int threshold; int count = 0; };
// Trampoline: C linkage, recovers state from void*
extern "C" void tally_above(void* ctx, int value) {
auto* c = static_cast<Context*>(ctx);
if (value > c->threshold) ++c->count;
}
// Hypothetical C API: void foreach_int(int*, size_t, void(*)(void*,int), void*)
// foreach_int(data, 6, tally_above, &ctx);Never pass a capturing lambda directly where a C API expects a function pointer — the conversion is ill-formed and produces a compile error. Use the void* context slot when the API provides one; if it does not, a static or thread-local trampoline variable is the only portable option.
Type aliases for function pointers
using aliases make function pointer types readable; typedef buries the alias name inside the declarator syntax. Both produce identical types; using is strictly preferred since C++11.
// Preferred: alias name is always on the left // C++11
using Comparator = int (*)(const void*, const void*);
using EventHandler = void (*)(int event_id, void* ctx);
using Transform = double (*)(double) noexcept; // C++17 — noexcept in type
// Legacy typedef equivalents (identical semantics, harder to read)
typedef int (*Comparator_t)(const void*, const void*);
typedef void (*EventHandler_t)(int, void*);
// Alias template for parameterised callbacks // C++11
template <typename T>
using Predicate = bool (*)(const T&);
Predicate<int> is_even = [](const int& n) { return n % 2 == 0; };
Predicate<double> is_pos = [](const double& x) { return x > 0.0; };
// Using-alias for member function pointers
using TimerStart = void (Timer::*)(); // see Syntax section aboveAlias templates (template <typename T> using ...) work for function pointer types just as they do for any other type, enabling lightweight parameterised callback interfaces without dragging in std::function.
Storing heterogeneous callables
When you need a single container to hold callables with different concrete types but the same signature, type erasure is unavoidable. std::function is the standard vocabulary type; std::move_only_function (C++23) covers move-only captures.
#include <functional>
#include <vector>
// Homogeneous container of type-erased callables // C++11
std::vector<std::function<void()>> pipeline;
int counter = 0;
pipeline.emplace_back([] { std::puts("step 1"); });
pipeline.emplace_back([&counter] { ++counter; }); // capturing lambda
pipeline.emplace_back([] { std::puts("done"); });
for (auto& step : pipeline) step();
// Move-only captures require std::move_only_function // C++23
#include <memory>
std::vector<std::move_only_function<void()>> tasks;
tasks.emplace_back([p = std::make_unique<int>(1)] { /* use p */ });
tasks.emplace_back([p = std::make_unique<int>(2)] { /* use p */ });
for (auto& t : tasks) t();For heterogeneous callables that must also be type-safe at retrieval time, consider std::any paired with std::any_cast, or a tagged union of known callable types. If the set of callable types is closed and known at compile time, std::variant with std::visit avoids all heap allocation.
Function pointers and templates
Template parameters accept any callable — raw pointer, lambda, or functor — at zero runtime cost; the compiler instantiates a specialisation per concrete type and can inline the body. This is the highest-performance callback mechanism in C++.
#include <concepts>
#include <vector>
#include <functional>
// C++11: unconstrained template — accepts any callable
template <typename F>
void for_each_int(const std::vector<int>& v, F&& f) {
for (int x : v) f(x);
}
// C++20: constrained template — same codegen, better error messages
template <std::invocable<int> F>
void for_each_checked(const std::vector<int>& v, F&& f) {
for (int x : v) std::invoke(f, x);
}
// Passing a raw function pointer — still just a template instantiation
void print_int(int x) { std::printf("%d\n", x); }
std::vector<int> nums{1, 2, 3};
for_each_int(nums, print_int); // T = void(*)(int)
for_each_int(nums, [](int x) { return x * 2; }); // T = unique lambda type
// Non-type template parameter for compile-time-constant callbacks // C++20
template <auto Fn>
void call_twice(int x) {
Fn(x); Fn(x);
}
call_twice<print_int>(42); // C++20 — Fn baked into the binary as a constantThe auto non-type template parameter (C++20) allows a function pointer to be embedded directly in the template instantiation rather than loaded at runtime, enabling the compiler to inline across the indirect call even without link-time optimisation.
See Also
- Lambdas — preferred callable syntax since C++11; replaces most function pointer and functor use
- Templates — zero-cost callback mechanism via template parameters
- Type Erasure — the pattern underlying
std::functionand similar wrappers - Ranges —
std::rangesalgorithms are constrained withstd::invocable