Course outline

C++ / Intermediate / Lesson 10

Templates, lambdas, and reusable operations

Generalize a useful operation across types, then pass behavior as a callable while making captured state and its lifetime explicit.

About 5 min readContent reviewed
On this page

Lesson goals

  • Read function templates and understand deduction at a call.
  • Use lambda captures by value and by reference intentionally.
  • Keep generic requirements and template definitions visible.

A family of functions, not one runtime switch

A function template describes a family of functions parameterized by types or values. The compiler forms a specialization when a use requires one. Type deduction matches parameter patterns against arguments; it does not simply convert every argument to a convenient common type. A template taking two T parameters may reject an int and a double because they imply different T values. State the operation you require from T instead of claiming a template works for every type. The add_pair example requires meaningful addition and a result convertible to T.

One definition, two valid specializations

The first call deduces T as int and the second as std::string. Addition means numeric addition for the integers and concatenation for the strings; those meanings come from the operand types. The chosen integer values do not overflow. Template syntax does not make a dangerous operation safe or prove algebraic laws. Document whether order matters and whether the operation can throw. In production interfaces, concepts can express some required syntax and type properties, but tests and documentation still explain semantic expectations that the compiler cannot establish.

cpp Generic syntax preserves type meaning
#include <iostream>
#include <string>
template<class T>
T add_pair(const T& left, const T& right) { return left + right; }
int main() {
    std::cout << add_pair(4, 7) << "\n";
    std::cout << add_pair(std::string{"sun"}, std::string{"rise"}) << "\n";
}
Expected output
11
sunrise

Both arguments in each call deduce the same T. The instantiated bodies use the corresponding addition operation.

A lambda is an object with captured state

A lambda expression creates a closure object with a call operator. Capturing offset by value stores a copy in that closure; capturing by reference borrows an existing object. A reference capture does not extend the object lifetime, so a returned callback must not borrow a dead local. Prefer explicit capture lists when ownership matters. The transform example copies an offset of three into its lambda, then writes one output element per input element. The destination vector is sized first because transform writes existing elements rather than appending automatically.

cpp A copied capture and a valid output range
#include <algorithm>
#include <iostream>
#include <vector>
int main() {
    const std::vector<int> input{1, 2, 3};
    std::vector<int> output(input.size());
    const int offset{3};
    std::transform(input.begin(), input.end(), output.begin(),
        [offset](int value) { return value + offset; });
    for (int value : output) { std::cout << value << " "; }
    std::cout << "done\n";
}
Expected output
4 5 6 done

Each small integer increases by three. The closure owns its offset copy, and the output vector has three writable elements.

Choose how behavior crosses an interface

A template parameter can accept different callable types and often lets the optimizer see the exact call target. std::function provides a uniform owning wrapper for compatible copyable callables in C++20, with potential allocation and indirect-call costs. A function pointer handles compatible free functions and captureless lambdas, but cannot store arbitrary captured state. Choose based on lifetime and interface requirements rather than presumed speed. If a callback escapes into a queue, its captures must remain valid until execution, and the queue must specify cancellation and failure behavior.

Definitions must be available where needed

Ordinary template definitions usually belong in headers so each translation unit can instantiate the specializations it uses. Merely declaring a template in a header and placing its definition in an unrelated source file commonly leads to missing specializations at link time. Explicit instantiation can support a deliberately closed set of types, but it must be designed consistently. When diagnostics expand through many template layers, start with the user-level call and the first unsatisfied operation. Reduce the example to the smallest type and callable that reproduce the issue before adding more abstraction.

Common errors and fixes

A callback reads a destroyed local.

Why it happens
A reference capture escaped its owner scope.
How to fix it
Capture an owned value or an appropriate owning handle.
Prevent it next time
Review capture lifetimes whenever callbacks are stored.

transform writes beyond an empty destination.

Why it happens
reserve was mistaken for creating output elements.
How to fix it
Resize output or use a suitable inserter.
Prevent it next time
Prove the output range is writable before calling.

A template specialization is undefined at linking.

Why it happens
Its definition was unavailable during required instantiation.
How to fix it
Expose the definition or provide the intended explicit instantiation.
Prevent it next time
Test template headers from an independent translation unit.

Check your understanding

1. Predict add_pair(2, 3) and explain why add_pair(2, 3.5) does not fit its single-T deduction rule. Propose a deliberate call.

Show worked solution

The first call returns five with T equal to int. The mixed call proposes both int and double for T, so deduction conflicts. add_pair<double>(2, 3.5) explicitly selects double and returns 5.5 after conversion. Decide whether accepting mixed numeric types belongs in the API rather than adding conversions merely to silence an error.

2. A factory returns a lambda that adds its local offset by reference. Redesign the capture and describe the result for offset four and argument six.

Show worked solution

Capture offset by value with [offset], so the closure stores its own integer copy. Returning the closure no longer leaves a reference to the factory local. Calling it with six returns ten for this representable input. If later updates to shared state are required, that is a different contract and needs an explicit lifetime and synchronization design.

Key takeaways

  • Templates require real operations, not arbitrary types.
  • Captures are ownership and lifetime decisions.
  • Keep instantiation definitions and valid output ranges available.

Official references