C++ Notes
; like any other statement.std::sort or std::find_if, in the cleanest possible way.[capture list] (parameters) -> return type {
// code
};
[]: stores already-defined variables from the surrounding scope into the lambda, captured once, at lambda creation time. [a, b] captures a and b by value (copy), changes to the originals afterward don't affect the lambda. [&a, &b] captures by reference, the lambda sees changes to the originals, which is dangerous if the original goes out of scope before the lambda is called. [=] captures every used variable by value, [&] captures every used variable by reference. Optional, leave it empty [] if no outside variables are needed.(): normal function parameters, passed every time the lambda is called. Optional.-> type: optional, the compiler can deduce it in most cases. If explicitly stated, the lambda must actually return that type.auto to store a lambda. Each lambda has a unique, unnamed type that can't be written manually.().() right after the lambda body, it's called immediately, and auto then stores the return value, not the function object.auto func = [](int x, int y){ return x + y; }; // semicolon, NOT called yet // call later, multiple times func(10, 20); // returns 30 func(5, 3); // returns 8
auto result = [](int x, int y){ return x + y; }(10, 20); // called right here // result = 30 (an int, not a lambda) // function object is gone
auto func = [](){ // code }; func(); // call when needed
[](){
// runs once, cannot reuse
}();auto func = []() -> int { return 5; };
auto func = [](int a, int b){ return a + b; }; func(10, 20);
int limit = 10; auto func = [limit](int x){ return x > limit; }; // limit is a copy
int counter = 0; auto inc = [&counter](){ counter++; }; // modifies original
int a=1, b=2; auto func = [=](){ return a + b; }; // a, b copied at creation
int a=1, b=2; auto func = [&](){ a++; b++; }; // modifies originals
sort(v.begin(), v.end(), [](int a, int b){ return a < b; }); // not stored, used inline
auto func = [](int x){ std::cout << x; }; // side-effect lambda
#include <iostream> int main() { // 1) named lambda: reusable function object auto square = [](int x) { return x * x; }; std::cout << square(4) << std::endl; // prints: 16 std::cout << square(7) << std::endl; // prints: 49 // 2) lambda with multiple statements auto doubleAndAdd = [](int x) { int y = x * 2; y = y + 3; return y; }; std::cout << doubleAndAdd(5) << std::endl; // prints: 13 // 3) lambda with no parameters auto sayHello = []() { std::cout << "Hello from lambda" << std::endl; }; sayHello(); // prints: Hello from lambda // 4) explicit return type auto getNumber = []() -> int { return 10; }; std::cout << getNumber() << std::endl; // prints: 10 // 5) capture by value: limit is copied at lambda creation int limit = 5; auto isGreater = [limit](int x) { return x > limit; }; std::cout << isGreater(10) << std::endl; // prints: 1 (true) std::cout << isGreater(3) << std::endl; // prints: 0 (false) // 6) capture by reference: counter is modified through lambda int counter = 0; auto increase = [&counter]() { counter++; }; increase(); increase(); std::cout << counter << std::endl; // prints: 2 // 7) immediately invoked: runs once, function object not stored []() { std::cout << "This runs immediately" << std::endl; }(); // prints: This runs immediately return 0; }
template <typename T, typename U> T myMax(T a, U b) { return (a > b) ? a : b; }
template <typename T> line.T, U) are scoped to their own template only.myMax<int, double>(22, 11);, the compiler may do implicit conversion as needed.& in the function parameter, not inside the angle brackets.T represent types. References are applied to variables, not types.template <typename T, typename U> T myMaxRef(T& a, U& b) { // & on the variables, not inside <> return (a > b) ? a : b; }
Avoid defining two nearly identical templates, one taking values, one taking const references. Calling either can trigger ambiguity where the compiler can't decide which overload to use.
const char* strings, since the default > would compare addresses, not content.<> after template are left empty, the actual type goes after the function name instead.// primary template template <typename T> T maximumSpecial(T a, T b) { return (a > b) ? a : b; } // specialization for const char* (compare content, not address) template <> const char* maximumSpecial<const char*>(const char* a, const char* b) { return (std::strcmp(a, b) > 0) ? a : b; }
#include <iostream> #include <cstring> // 1) basic template: T and U are deduced from arguments template <typename T, typename U> T myMax(T a, U b) { return (a > b) ? a : b; } // 2) single type template template <typename T> T add(T a, T b) { return a + b; } // 3) by reference template template <typename T, typename U> T myMaxRef(T& a, U& b) { return (a > b) ? a : b; } // 4) primary template + specialization for const char* template <typename T> T maximumSpecial(T a, T b) { return (a > b) ? a : b; } template <> const char* maximumSpecial<const char*>(const char* a, const char* b) { return (std::strcmp(a, b) > 0) ? a : b; // compares content, not address } int main() { std::cout << myMax(10, 5.5) << std::endl; // prints: 10 std::cout << myMax(7, 3) << std::endl; // prints: 7 std::cout << add(5, 7) << std::endl; // prints: 12 // add(5, 3.2); // ERROR: T deduced as both int and double std::cout << myMax<int, double>(10, 5.5) << std::endl; // explicit types: prints 10 int x = 20; double y = 15.2; std::cout << myMaxRef(x, y) << std::endl; // by reference: prints 20 const char* s1 = "apple"; const char* s2 = "banana"; std::cout << maximumSpecial(s1, s2) << std::endl; // specialization used: prints banana return 0; }
The problem with plain templates: when you write a template, the compiler accepts any type. Pass the wrong one, and the error message is a wall of confusing internal compiler text pointing inside the template machinery, not at your code. Concepts let you say upfront, "this template only works with integers" or "only with types that support multiplication." Pass the wrong type, and the compiler gives a clean, readable error pointing directly at the call site instead.
#include <concepts>.std::integral (int, short, long, char, bool, etc.), std::floating_point (float, double, long double), std::same_as<T> (type must be exactly T), std::convertible_to<T> (type must be convertible to T).template <typename T> requires std::integral<T> T add(T a, T b) { return a + b; }
template <typename T> T add(T a, T b) requires std::integral<T> { return a + b; }
template <std::integral T> T add(T a, T b) { return a + b; }
auto add(std::integral auto a, std::integral auto b) { return a + b; }
#include <type_traits> template <typename T> concept MyIntegral = std::is_integral_v<T>; // same as std::integral template <typename T> requires MyIntegral<T> T add(T a, T b) { return a + b; }
template <typename T> concept Multipliable = requires(T a, T b) { a * b; // only checks that this expression is valid for T // does NOT check the result value, only that it compiles };
template <typename T> concept Incrementable = requires(T a) { a += 1; ++a; a++; }; // T must support all three of these
Just checks that an expression is syntactically valid. Does not check whether the result is true.
template <typename T> concept TinyType = requires(T t) { sizeof(T) <= 4; // only checks this expression compiles, NOT that it equals true };
Uses an inner requires keyword to check that an expression is actually true at compile time, not just syntactically valid.
template <typename T> concept TinyType = requires(T t) { sizeof(T) <= 4; // syntax check only requires sizeof(T) <= 4; // actual boolean value check };
Checks three things at once: the expression compiles, it does not throw exceptions, and the result is convertible to a specific type.
template <typename T> concept Addable = requires(T a, T b) { { a + b } noexcept -> std::convertible_to<int>; // a + b must: compile, not throw, and result must convert to int };
&&: all conditions must be satisfied.||: any one condition is enough.// || : works for integers OR floating-point types template <typename T> T func(T t) requires std::integral<T> || std::floating_point<T> { return 2 * t; } // && : works only for types that are BOTH integral AND TinyType template <typename T> requires std::integral<T> && TinyType<T> T add(T a, T b) { return a + b; }
auto accepts any type. You can constrain what it's allowed to deduce using concepts.// constrained function parameters std::integral auto add(std::integral auto a, std::integral auto b) { return a + b; } // constrained variables std::integral auto x = 10 + 20; // ok: deduces int std::integral auto sum = add(10, 20); // ok: result is integral // std::integral auto f = 3.14; // ERROR: double is not integral
Think of concepts like a gatekeeper at the template door. Without concepts, anyone walks in, and the error surfaces deep inside the template machinery. With concepts, the gatekeeper checks the type at the entrance and gives a clear rejection message right where you called the function.
That closes out functions in both directions: the plain mechanics of passing data across a boundary, and the more modern tools, lambdas, templates, and concepts, that let a single function body work across many types safely. Next up, classes and objects, where functions stop standing alone and start belonging to data.