Modern C++ Fundamentals: From Basics to Smart Pointers and Threads · Modern C++ Basics: Types, Containers, Functions and Classes
Functions, Lambdas and the Standard Algorithms
Overload functions, use default arguments, write lambdas with captures, and replace hand-written loops with std::sort, std::find_if, std::transform and std::accumulate.
What you'll build
A product-catalog tool that filters, sorts, totals and transforms a list of products using lambdas and standard algorithms instead of manual loops.
The idea
- Overloading: several functions with the same name and different parameter types. The compiler picks the best match.
- Default arguments:
void log(std::string_view msg, int level = 1). - Lambdas are small anonymous functions:
[captures](parameters) { body }. A capture list decides which outside variables the lambda can use:[]none,[=]copies,[&]references,[threshold]one by value,[&total]one by reference. std::function<R(Args...)>can hold any callable with that signature; use it to store callbacks. For parameters in templates, a plain template parameter is faster.- Standard algorithms (
<algorithm>,<numeric>) express intent:std::sort,std::find_if,std::count_if,std::transform,std::remove_if,std::accumulate. They work on iterator pairs, such asv.begin(), v.end(). - Erase-remove:
std::erase_if(v, pred)(C++20) removes matching elements from a vector in one call.
Prefer an algorithm with a clear name over a loop that someone must read to understand.
The code
#include <algorithm>
#include <iostream>
#include <numeric>
#include <string>
#include <vector>
struct Product {
std::string name;
double price;
int stock;
};
// Overloads: same name, different parameters
std::string describe(const Product& p) { return p.name + " ($" + std::to_string(static_cast<int>(p.price)) + ")"; }
std::string describe(const std::vector<Product>& items) {
std::string out;
for (const auto& p : items) out += describe(p) + " ";
return out;
}
// Default argument
void print_header(const std::string& title, int width = 24) {
std::cout << std::string(static_cast<size_t>(width), '-') << '\n' << title << '\n';
}
int main() {
std::vector<Product> catalog{
{"Keyboard", 49.0, 12}, {"Mouse", 19.0, 0}, {"Monitor", 189.0, 4}, {"Cable", 5.0, 100}, {"Webcam", 59.0, 0},
};
print_header("In stock, cheapest first");
std::vector<Product> in_stock = catalog;
std::erase_if(in_stock, [](const Product& p) { return p.stock == 0; });
std::sort(in_stock.begin(), in_stock.end(), [](const Product& a, const Product& b) { return a.price < b.price; });
std::cout << describe(in_stock) << '\n';
// Captures: use a local variable inside the lambda
const double budget = 60.0;
auto affordable = std::count_if(catalog.begin(), catalog.end(),
[budget](const Product& p) { return p.price <= budget; });
std::cout << affordable << " products cost at most " << budget << '\n';
// find_if returns an iterator; compare with end() before dereferencing
auto it = std::find_if(catalog.begin(), catalog.end(), [](const Product& p) { return p.price > 100; });
if (it != catalog.end()) std::cout << "first over $100: " << it->name << '\n';
// accumulate totals with a custom operation
const double stock_value = std::accumulate(catalog.begin(), catalog.end(), 0.0,
[](double sum, const Product& p) { return sum + p.price * p.stock; });
std::cout << "stock value: " << stock_value << '\n';
// transform into a new vector
std::vector<std::string> names;
std::transform(catalog.begin(), catalog.end(), std::back_inserter(names), [](const Product& p) { return p.name; });
std::cout << names.size() << " names, last: " << names.back() << '\n';
// A mutable counter captured by reference
int visited = 0;
std::for_each(catalog.begin(), catalog.end(), [&visited](const Product&) { ++visited; });
std::cout << "visited " << visited << '\n';
}
Output
------------------------ In stock, cheapest first Cable ($5) Keyboard ($49) Monitor ($189) 4 products cost at most 60 first over $100: Monitor stock value: 1844 5 names, last: Webcam visited 5
Run this on your computer — see “Get Started” for setup.
Common mistakes
- Capturing by reference a variable that outlives its scope. A lambda stored or returned after its captured locals are gone has dangling references; capture by value.
- Using
[=]and[&]carelessly. Prefer explicit captures so readers see what a lambda depends on. - Dereferencing
find_ifresults without checking!= end(). - Calling
std::remove_ifand forgetting toerase.remove_ifonly moves elements;std::erase_if(C++20) or the erase-remove idiom actually shrinks the container. - Passing a lambda as
std::functionin hot loops. It may allocate and block inlining; use a template parameter orautofor performance-sensitive code.
Key points
- Overload functions by parameter type; use default arguments for optional values.
- Lambdas are
[captures](params) { body }; list captures explicitly. - Standard algorithms (
sort,find_if,count_if,transform,accumulate,erase_if) state intent more clearly than loops. - Always compare iterators from
findtoend()before dereferencing.
Further reading: cppreference: Algorithms library.
Practice questions
What is the difference between capturing with `[=]`, `[&]` and `[x]`?
Answer
[=] copies every outside variable the lambda uses, [&] refers to them by reference, and [x] copies only x. Explicit captures like [x] or [&total] make dependencies visible and avoid accidental sharing or dangling references.
Why doesn't std::remove_if alone shrink a vector?
Answer
It only moves the elements to keep to the front and returns the new logical end; the container's size is unchanged. You must call erase on the leftover range, or use std::erase_if (C++20) which does both.
Open in the app to mark lessons complete, take notes and track your progress.