Skip to content
C++
Language
since C++98
Intermediate

Function Pointers and Callables

Raw function pointers, member function pointers, std::function, std::invoke, and zero-cost template callbacks in modern C++.

Callablesince C++98

A 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, noexcept becomes 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

cpp
// 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++17

A 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

cpp
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";    // write

Member 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:

cpp
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)

cpp
#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:

cpp
#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 afterward

std::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:

cpp
#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)

cpp
#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:

cpp
// 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

cpp
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 F for zero-overhead callbacks: when the callable type is resolved at the call site, pass F&& and call via std::invoke. The compiler inlines; virtual dispatch is eliminated.
  • std::function for 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 a unique_ptr, file descriptor, or other non-copyable resource requires this over std::function.
  • using over typedef for 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::function or 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.

cpp
std::function<void()> f;   // null
f();                        // throws std::bad_function_call

Dangling 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.

cpp
std::function<void()> handler;
{
    int local = 42;
    handler = [&local] { std::print("{}\n", local); };
}
handler();   // UB: local is destroyed

std::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.

cpp
#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 indirection

The 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.

cpp
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 pointer

The ->* 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.

cpp
#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.

cpp
// 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 above

Alias 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.

cpp
#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++.

cpp
#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 constant

The 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::function and similar wrappers
  • Ranges — std::ranges algorithms are constrained with std::invocable