Function Pointers, Lambdas, and std::function in Modern C++

Function pointers and lambdas are two ways to treat a function as a value — something you can store in a variable, pass to another function, or return from a function. In this post I explain both, how they relate to std::function, and when to reach for each one.


Function Pointers

A function pointer holds the address of a function. Calling through a function pointer dispatches to whichever function is stored at runtime.

Basic Syntax

The declaration syntax is famously confusing. The key rule: the pointer name sits in the middle, wrapped in parentheses, with the return type on the left and parameter types on the right.

int add(int a, int b) { return a + b; }
int mul(int a, int b) { return a * b; }

// Declare a pointer to a function that takes two ints and returns int
int (*op)(int, int);

op = add;       // point at add
op(3, 4);       // returns 7

op = mul;       // point at mul
op(3, 4);       // returns 12

You can skip the & when assigning a function — a function name decays to a pointer to itself, the same way an array name decays to a pointer to its first element.

Cleaning Up the Syntax with using

The raw pointer syntax scales poorly when used as a parameter type. Use a type alias:

using BinaryOp = int (*)(int, int);

int apply(BinaryOp op, int a, int b) {
    return op(a, b);
}

apply(add, 3, 4); // 7
apply(mul, 3, 4); // 12

Passing Functions as Callbacks

Function pointers are the classic C-style callback mechanism. The C standard library uses them throughout:

#include <cstdlib>

int cmp(const void* a, const void* b) {
    return *(int*)a - *(int*)b;
}

int arr[] = {5, 2, 8, 1};
qsort(arr, 4, sizeof(int), cmp); // sorts in place

Limitations

  • Can only point to free functions (or static member functions). Cannot point to a non-static member function or capture local state.
  • No inline opportunity at a call site unless the compiler can prove at compile time which function is pointed to.

Lambdas (C++11)

A lambda is an anonymous function object defined inline. The compiler synthesizes a unique struct for each lambda, with operator() containing your lambda body. This means lambdas are zero-cost abstractions when used with templates — the compiler can see the full body and inline it.

Basic Syntax

[capture](parameters) -> return_type { body }

The -> return_type is optional; the compiler deduces it from the return statement.

auto square = [](int x) { return x * x; };
square(5); // 25

// Used inline with std::sort
vector<int> v = {5, 2, 8, 1};
sort(v.begin(), v.end(), [](int a, int b) { return a < b; });

Capture Clauses

The capture clause [...] controls which local variables from the enclosing scope the lambda can access.

CaptureMeaning
[]Capture nothing
[=]Capture all locals by value (copy)
[&]Capture all locals by reference
[x]Capture x by value
[&x]Capture x by reference
[=, &x]Capture all by value except x by reference
int threshold = 10;

// By value: lambda gets its own copy of threshold
auto above_val = [threshold](int x) { return x > threshold; };

// By reference: lambda reads threshold through a reference
// (careful: if threshold goes out of scope, this is UB)
auto above_ref = [&threshold](int x) { return x > threshold; };

threshold = 20;
above_val(15); // still uses 10 → false
above_ref(15); // uses current threshold (20) → false

Prefer explicit captures ([x], [&x]) over blanket [=] or [&] — they make it clear exactly what state the lambda depends on.

Mutable Lambdas

By default, variables captured by value are const inside the lambda body. Use mutable to allow modification of the copy:

int count = 0;
auto increment = [count]() mutable { return ++count; };

increment(); // 1 (modifies the lambda's own copy)
increment(); // 2
count;        // still 0 — original is untouched

Generic Lambdas (C++14)

Use auto parameters to write a lambda that works for multiple types:

auto add = [](auto a, auto b) { return a + b; };

add(1, 2);       // int: 3
add(1.5, 2.5);   // double: 4.0
add(string("hello "), string("world")); // string concatenation

The compiler generates a separate template instantiation for each distinct set of argument types — identical to a function template.


std::function

std::function<R(Args...)> is a type-erased callable wrapper. It can hold any callable with a compatible signature: a free function, a lambda (with or without captures), a functor, or a bound member function.

#include <functional>

using BinaryOp = function<int(int, int)>;

BinaryOp op;

op = add;                                   // free function
op = [](int a, int b) { return a * b; };    // lambda
op = multiplier_object;                     // functor with operator()

When to Use std::function

Use it when you need to store a callable in a struct or class member, or return one from a function, and the concrete type varies at runtime:

struct Button {
    function<void()> on_click;
};

Button b;
b.on_click = []() { cout << "clicked!\n"; };
b.on_click(); // dispatches at runtime

The Performance Cost

std::function is not free:

  1. Type erasure overhead: uses an internal virtual-dispatch-like mechanism to call through to the stored callable.
  2. Possible heap allocation: if the stored callable is larger than a small internal buffer (implementation-defined, typically 16–32 bytes), std::function heap-allocates it.
  3. Cannot be inlined: the compiler cannot see through std::function to inline the body.

Prefer a template parameter over std::function in performance-critical code:

// Fast: compiler instantiates a version with the exact lambda type and can inline
template <typename F>
int apply(F op, int a, int b) {
    return op(a, b);
}

// Slower: virtual dispatch, possible allocation, no inlining
int apply(function<int(int,int)> op, int a, int b) {
    return op(a, b);
}

How They Relate

                    ┌──────────────────────────────┐
                    │        std::function         │  ← holds any callable
                    │  (type erasure, heap alloc)  │
                    └──────────────┬───────────────┘
                                   │ wraps
              ┌────────────────────┼────────────────────┐
              ▼                    ▼                    ▼
      free function            lambda []           lambda [x]/[&]
      (function ptr)      (no capture → can        (has state →
                          convert to fn ptr)       cannot convert)

Key rules:

  • A lambda with an empty capture [] is implicitly convertible to a function pointer of matching type.
  • A lambda with any capture is not convertible to a function pointer — it carries state and has no single address.
  • std::function wraps all of the above at the cost of type erasure.
using Op = int (*)(int, int);

Op p = [](int a, int b) { return a + b; }; // OK: no capture

int x = 5;
Op q = [x](int a, int b) { return a + x; }; // ERROR: has capture

Summary

Function pointerLambdastd::function
Holds state?NoYes (via capture)Yes
Inlineable?RarelyYes (with templates)No
SyntaxVerboseConciseConcise
OverheadIndirect callZero (template)Type erasure + possible alloc
Use whenC APIs, callbacksLocal use, STL algorithmsStoring mixed callables

Default advice:

  • Use a lambda for STL algorithms and local callbacks.
  • Use a template parameter (template <typename F>) when writing a higher-order function you want the compiler to optimize.
  • Use std::function only when you need to store a callable whose concrete type is not known at compile time (e.g., event handlers, plugin systems).
  • Use a function pointer when interfacing with C APIs that require one.