C++ Lambdas, `auto`, and Type Deduction
- Description: A note on C++ lambdas (captures, mutable, generic,
constexpr/consteval),auto/decltype/decltype(auto), and CTAD (Class Template Argument Deduction) - My Notion Note ID: K2A-B1-11
- Created: 2018-10-05
- Updated: 2026-02-28
- License: Reuse is very welcome. Please credit Yu Zhang and link back to the original on yuzhang.io
Table of Contents
- 1. Lambda Basics
- 2. Captures
- 3.
mutableLambdas - 4. Generic Lambdas (C++14)
- 5.
constexprandconstevalLambdas - 6.
autoType Deduction - 7.
decltype - 8.
decltype(auto) - 9. Class Template Argument Deduction (CTAD, C++17)
1. Lambda Basics
- Lambda = anonymous function object. Compiler synthesizes a unique class with
operator()+ (optionally) captured members.
auto add = [](int a, int b) { return a + b; };
add(2, 3); // 5
// Anatomy:
// [captures](parameters) -> return_type { body }
//
// Most parts are optional:
// [] -- no captures
// () -- no parameters (can omit if there are no params)
// -> T -- explicit return type (often deduced)
- Each lambda has a unique compiler-generated type → two lambdas are always distinct types, even if they look identical.
2. Captures
- Captures bring outside variables into the lambda body.
2.1 Capture by Value vs by Reference
int x = 10;
auto by_value = [x] { return x; }; // copies x at lambda creation
auto by_ref = [&x] { return x; }; // refers to x; reflects changes
x = 20;
by_value(); // 10 (frozen)
by_ref(); // 20 (live)
- Rule: captured-by-reference object must outlive the lambda.
- Common bug: return lambda that captures local by reference → dangling reference when fn returns.
2.2 Default Captures ([=], [&])
[=] // capture all used variables by value
[&] // capture all used variables by reference
[=, &z] // by value except z by reference
[&, x] // by reference except x by value
- Recommend: name captures explicitly (
[x, &y]). Defaults hide what's borrowed/owned → bug-prone in long lambdas.
2.3 Init Captures (C++14)
- Introduce new variables in capture list. For moves + computed values.
auto big = std::make_unique<Data>();
auto lambda = [data = std::move(big)] { // move into the lambda
return data->size();
};
auto counter = [n = 0]() mutable { // start state
return ++n;
};
counter(); // 1
counter(); // 2
2.4 Capturing *this
[this]— capturesthispointer (access viathis->). Dangles if object dies first.[*this](C++17) — copies entire object into lambda; self-contained.
class Widget {
int value_ = 0;
public:
auto by_pointer() {
return [this] { return value_; }; // borrows *this
}
auto by_copy() {
return [*this] { return value_; }; // copies *this
}
};
3. mutable Lambdas
- Lambda's
operator()isconstby default → captured-by-value vars can't be modified inside body. mutableremoves theconst.
int x = 10;
auto a = [x] { return x; }; // OK: read-only
auto b = [x] { x = 20; return x; }; // ERROR: can't modify x
auto c = [x]() mutable { x = 20; return x; }; // OK: mutable
mutableonly affects captures-by-value. Captures-by-reference can always modify referent (reference itself isconst, referent isn't).
4. Generic Lambdas (C++14)
autoparams → generic lambda. Compiler synthesizes a templatedoperator().
auto print = [](const auto& x) { std::cout << x; };
print(42);
print("hello");
print(std::vector{1, 2, 3}); // would need an overload of <<
auto add = [](auto a, auto b) { return a + b; };
add(1, 2); // int
add(1.0, 2); // double
add(std::string("hi "), "world");
- C++20: explicit template parameters:
auto sum = []<typename T>(const std::vector<T>& v) {
T total{};
for (const auto& x : v) total += x;
return total;
};
5. constexpr and consteval Lambdas
- Implicitly
constexprwhen body satisfiesconstexprrules. - Mark explicitly with
constexpr(C++17) orconsteval(C++20, compile-time-only).
constexpr auto square = [](int x) constexpr { return x * x; };
static_assert(square(5) == 25);
consteval auto cube = [](int x) { return x * x * x; };
constexpr int n = cube(3); // OK: compile-time
// int m = cube(runtime_x); // ERROR: must be compile-time
6. auto Type Deduction
autodeduces type from initializer via template argument deduction rules.
int x = 42;
auto a = x; // int (top-level const/ref/volatile dropped)
auto& b = x; // int& (reference preserved)
const auto& c = x; // const int& (you say const explicitly)
auto* d = &x; // int*
auto e = "hello"; // const char* (NOT std::string)
const int& cr = x;
auto f = cr; // int (drops const AND reference!)
auto& g = cr; // const int& (preserves both)
Key rules:
autoalone — drops references, top-levelconst,volatile.auto&/auto&&/const auto&— preserve references +constof source.auto*— requires a pointer.- Braced initializers (
auto x = {1, 2, 3};) →std::initializer_list, not the bracketed type.
7. decltype
decltype(expr)— exact declared type ofexpr, including references.
int x = 10;
const int& cr = x;
decltype(x) a = 0; // int
decltype(cr) b = x; // const int& (preserves the reference)
decltype((x)) c = x; // int& (parens around an lvalue → reference)
- Gotcha:
decltype(name)= declared type.decltype((name))= type of expression →T&for any lvalue.
8. decltype(auto)
decltype(auto)(C++14) — usesdecltyperules (preserving refs +const), inferred from initializer.
int& f();
auto x = f(); // int (reference dropped)
decltype(auto) y = f(); // int& (preserved)
template <typename Fn>
decltype(auto) wrap(Fn&& fn) {
return fn(); // returns by reference if fn returns by reference
}
- Use for perfect-return-type forwarding — wrapper returns exactly what wrapped fn returns, references and all.
9. Class Template Argument Deduction (CTAD, C++17)
- Omit class template args when compiler can deduce from constructor.
std::pair p{1, "hello"}; // pair<int, const char*>
std::vector v{1, 2, 3}; // vector<int>
std::tuple t{1, 'a', 2.0}; // tuple<int, char, double>
std::array a{1, 2, 3}; // array<int, 3>
std::lock_guard lock(mtx); // lock_guard<std::mutex>
- Library authors supply deduction guides when default deduction wouldn't pick right type:
template <typename Iter>
class Range {
public:
Range(Iter begin, Iter end);
};
// Deduction guide
template <typename Iter>
Range(Iter, Iter) -> Range<Iter>;
std::vector<int> v;
Range r(v.begin(), v.end()); // Range<vector<int>::iterator>
- Reduces noise. Doesn't replace explicit template args when deduction is ambiguous or wrong.