5.2.23 · D2C++ Programming

Visual walkthrough — std - function and std - bind

2,142 words10 min readBack to topic

This page goes deeper than the parent parent topic. If a word feels new, it is defined here before it is used.


Step 1 — What is a "callable"? (the raw ingredients)

WHAT. A callable is anything you can put a pair of parentheses after and hand arguments to. In C++ there are four species of them, and they are genuinely different animals to the compiler.

WHY start here. You cannot understand a box that "holds any callable" until you have seen the callables it must hold — and, crucially, seen that they have different types. That difference is the whole reason std::function exists.

PICTURE. Four shapes, four colours, four type names. Notice: even two lambdas that do the same thing have different unnamed types.

Figure — std - function and std - bind

The letters we will keep using:

  • ::: the return type (here int).
  • ::: the list of parameter types (here int, int).

Step 2 — The obstacle: different types can't share a variable

WHAT. We try, naïvely, to store all four in one variable — and fail.

WHY. A variable in C++ has one fixed type. add is a function pointer; a lambda is some hidden type __lambda_7; Mul is a struct. There is no single ordinary type that fits all of them, so the naïve auto x = add; x = someLambda; is a compile error.

PICTURE. Three coloured pegs (round, square, triangular) trying to enter one round hole. Two bounce off. The red ✗ marks the type mismatch.

Figure — std - function and std - bind

So we need a hole shaped by something all four share. What do they share?


Step 3 — The shared promise: the call signature

WHAT. Strip each callable down to only how you use it: give it two ints, get back one int. That shape — written — is the signature.

WHY this and not the type. The concrete types differ, but the usage shape is identical: f(2,3) -> int. If we build our box around the shape instead of the type, all four fit. This is the key idea that unlocks everything.

PICTURE. All four callables funnel through one stencil labelled int(int,int). What survives the stencil is only "2 ints in, 1 int out".

Figure — std - function and std - bind

Step 4 — Building the box: type erasure

WHAT. std::function<R(Args...)> is a class whose only job is: hold any callable matching that signature, and forget everything else about it. Forgetting the concrete type is called type erasure. See Templates and Type Erasure.

WHY it works. Internally std::function keeps a hidden pointer to the real callable plus a little table of "how to call this / copy this / destroy this". When you write f(2,3), it follows that pointer — an indirect call. It never needs to know what f really is, only that the stored thing answers to int(int,int).

PICTURE. A closed box labelled std::function<int(int,int)>. Inside sits whichever callable you assigned; on the outside is one universal call slot. Same box, different guts.

Figure — std - function and std - bind

Step 5 — The vector-of-callbacks payoff

WHAT. Because every std::function<void()> has the same type, you can line them up in a container. See Callbacks and Event Systems.

WHY it matters. A std::vector demands one element type. You cannot make std::vector<lambda> because there is no single lambda type (Step 1). Wrap each in std::function<void()> and they all become the same type — now they queue up neatly.

PICTURE. A row of identical boxes in a vector; each box hides a different task (save / log / render). One loop presses each button in turn.

Figure — std - function and std - bind
std::vector<std::function<void()>> tasks;
tasks.push_back([]{ std::cout << "save\n"; });
tasks.push_back([]{ std::cout << "log\n";  });
for (auto& t : tasks) t();   // run them all

Step 6 — Now the second machine: partial application (bind)

WHAT. Take a function of two inputs, . Freeze one input, say . What remains is a new function of one input: . That freezing is partial application, and std::bind performs it.

WHY a new tool. std::function stores; it does not reshape. Sometimes you have a 2-argument callable but the code that will call it can only supply 1 argument. You must pre-remember the missing one. That is exactly what bind does.

PICTURE. A 2-slot machine f(a,b). We plug the value 10 permanently into slot a. The machine now shows only one open slot — it became a 1-argument machine.

Figure — std - function and std - bind

Step 7 — Placeholders: wiring the remaining inputs

WHAT. For the slots you leave open, you write a placeholder _1, _2, … A placeholder is a routing label meaning "put the caller's n-th argument HERE."

WHY placeholders instead of just leaving a blank. Blanks could only mean "next argument in order". Placeholders are stronger: they let you reorder or even duplicate the caller's arguments. _1 = the caller's 1st argument, _2 = the caller's 2nd — regardless of where they sit in the bind expression.

PICTURE. A patch panel. On the left, the two arguments the caller will later supply, and . On the right, the two slots of sub(a,b). Coloured wires show how _1 and _2 route them — first straight, then crossed.

Figure — std - function and std - bind

Step 8 — The degenerate & member cases (never leave a gap)

WHAT. We cover the corners so no scenario surprises you.

WHY. A derivation is only trustworthy if it survives its edge cases: no placeholders, all placeholders, and the sneaky hidden-this of member functions.

PICTURE. Three mini-panels:

  1. Bind everythingbind(sub,10,3) → a zero-argument callable, g() = 7.
  2. Bind nothingbind(sub,_1,_2) → same arity, just a wrapper.
  3. Member function — the object slides in as the secret first argument (this).
Figure — std - function and std - bind

The one-picture summary

Two machines, one diagram: bind reshapes a callable's argument list; std::function stores whatever comes out under one uniform signature-typed roof, ready to be dropped in a container and called.

Figure — std - function and std - bind
Recall Feynman retelling — the whole walkthrough in plain words

We started with four kinds of "things you can call" — a plain function, lambdas, a struct with (), a member function — and noticed the compiler sees them as different types, so they can't share one variable (Steps 1–2). But they all feel the same when you use them: two ints in, one int out. We named that shared feeling the signature int(int,int) (Step 3). We then built a box, std::function, that hides the real type and remembers only "I can be called like the signature" — that hiding is type erasure (Step 4). Because every such box has the same type, we can line them up in a vector and run them all (Step 5). Then came a second, separate machine: bind, which takes a callable and freezes some inputs, shrinking how many you must supply later — partial application (Step 6). The leftover slots are wired with placeholders _1, _2, …, meaning "the caller's 1st, 2nd… argument", which even lets you reorder inputs (Step 7). Finally we swept the corners: freeze all inputs (arity 0), freeze none (a plain wrapper), and the sneaky member function whose object is the hidden first argument (Step 8). Modern C++ does the reshaping with lambdas — but the box, std::function, remains the one true way to store a callable of unknown concrete type.

Recall

What single thing do all four callable species share that std::function is built around? ::: The call signature R(Args...) — the usage shape, not the concrete type. Why must a vector of lambdas use std::function? ::: Each lambda has a unique unnamed type, so they can't share a vector element type; wrapping in std::function gives them one type. In std::bind(sub, 10, _1), what does _1 route? ::: The caller's first argument (supplied when the bound object is called), into sub's second parameter b. What does binding a member function need as its first bound argument? ::: The object (or pointer to it) — it fills the hidden this parameter. How do you pass by reference through bind? ::: Wrap with std::ref(x) / std::cref(x); bind copies by value otherwise.