C++ advanced: under the hood
Templates, the STL algorithms, lambdas, move semantics, the rule of zero/five, and real threads. This is where C++ stops being "a harder C" and becomes the language its designers intended.
You've written classes, managed resources with RAII, and leaned on the standard containers.1 Now we go underneath. C++'s power comes from three ideas working together: generics (write one algorithm for every type), moves (transfer resources instead of copying them), and concurrency (do several things at once, safely). The whole standard library is built on these. As always, every feature below links to cppreference.com2 or the C++ Core Guidelines.3 You should be comfortable with Parts 1 and 2 first.14
Type each example. The project at the end uses all of it.
01Templates: write once, work for any type
A template is a recipe the compiler uses to generate code for whatever type you plug in. A function template deduces its type from the arguments:5
template <typename T>
T max_of(T a, T b) {
return (a > b) ? a : b;
}
int i = max_of(3, 7); // T deduced as int
double d = max_of(2.5, 1.8); // T deduced as double
A class template parameterises a whole type, std::vector is
one. You can constrain templates with concepts (C++20) so errors are
readable instead of pages of template gibberish:6
template <typename T>
requires std::integral<T> // T must be an integer type
T half(T x) { return x / 2; }
std::cout << half(10); // OK: int is integral
// half(3.0); // error: double is not integral
The Core Guidelines treat templates as the default way to express "the same logic, many types", prefer them over copying code or reaching for macros.3
02The <algorithm> library
Before writing a loop by hand, check whether <algorithm> already does
it. These are generic, tested, and usually faster than a hand-rolled loop. They operate on
iterator pairs, the half-open range [begin, end).7
#include <algorithm>
#include <vector>
std::vector<int> v = {5, 2, 9, 1, 5};
std::sort(v.begin(), v.end()); // 1 2 5 5 9
auto it = std::find(v.begin(), v.end(), 9); // iterator to 9
bool has = (it != v.end());
int sum = 0;
std::for_each(v.begin(), v.end(), [&](int x) { sum += x; });
Iterators are what make templates and algorithms compose: an algorithm written for
vector works for array, map, or even a raw pointer
range, because they all speak the same iterator "language." Prefer the named algorithm
over a raw loop, it states your intent.3
03Lambdas: functions on the spot
A lambda is an unnamed function you write inline, ideal as the
"what to do" argument to an algorithm. The […] is the capture list:
[&] captures variables by reference, [=] by value.8
std::sort(v.begin(), v.end(),
[](int a, int b) { return a > b; }); // descending
int threshold = 4;
auto count = std::count_if(v.begin(), v.end(),
[threshold](int x) { return x > threshold; }); // captures by value
By default a lambda can't modify captured-by-value variables; add mutable to
allow it. To store a lambda in a variable, give it type
std::function<Return(Args)>.8
Lambdas are the modern replacement for many hand-written functors, and they're how
you make algorithms expressive.
04Move semantics: transfer, don't copy
Here's the key advanced idea. A copy duplicates data; a move takes ownership of it. When you return a big object or pass a temporary, C++ can "steal" its contents instead of cloning them, dramatically faster for strings, vectors, and containers.9
An rvalue reference (&&) binds to temporaries.
std::move(x) is a cast that says "I'm done with x, please take its
guts."10
std::vector<int> make_big() {
std::vector<int> v(1 << 20); // a million ints
return v; // moved, not copied
}
std::vector<int> a = make_big(); // the data is transferred
std::vector<int> b = std::move(a); // a is now empty; b owns the data
Understanding lvalues vs rvalues, and when the compiler chooses a move constructor
over a copy constructor, is what separates advanced C++ from intermediate.11
Pair it with std::forward in generic code to preserve whether an argument was
an lvalue or rvalue ("perfect forwarding").12
05The rule of zero (and five)
In Part 2 you learned RAII: tie a resource to an object's lifetime so it's cleaned up automatically.13 The modern conclusion is the rule of zero: if your class only holds smart pointers, containers, and strings, all of which already manage themselves, then you write no destructor, copy, or move code at all. The compiler generates correct ones.3
struct Document { // rule of zero
std::string title;
std::vector<char> data;
std::unique_ptr<std::string> note; // owns a heap object
}; // copies, moves, and cleanup: all correct, for free
Only when you directly own a raw resource (a file handle, a newed
pointer, a lock) do you fall back to the rule of five: define (or
= default/= delete) the destructor, copy/move constructors, and
copy/move assignment, as a set.14 In
practice: prefer the rule of zero; reach for std::unique_ptr so you almost
never need the rule of five at all.3
06Concurrency with std:thread
C++11 gave us portable threads in the standard library. A std::thread runs a
function concurrently with the rest of your program.15
You must join() (wait for it to finish) or detach() (let it run
free) before it's destroyed, RAII saves you here too, via
std::jthread in C++20, which joins automatically.
#include <thread>
#include <iostream>
void work(int id) {
std::cout << "thread " << id << " running\n";
}
std::thread t1{work, 1};
std::thread t2{work, 2};
t1.join(); // wait for t1
t2.join(); // wait for t2
07Sharing data safely: races and atomics
Threads sharing a variable without coordination produce
data races: undefined behaviour. The simplest fix for a single
counter is std::atomic, which makes reads/writes indivisible:16
#include <atomic>
std::atomic<int> counter{0};
void bump() { for (int i = 0; i < 1000; i++) counter++; }
std::thread a{bump}, b{bump};
a.join(); b.join();
std::cout << counter << std::endl; // reliably 2000
For richer shared state you'd use a std::mutex (lock with
std::lock_guard) so only one thread enters a critical section at a time. The
Core Guidelines are strict: never share writable state between threads without a
synchronisation mechanism, and prefer the highest-level tool that works (atomics >
mutex > hand-rolled flags).3
08Project: a parallel word-frequency counter
Let's combine everything: templates, algorithms, lambdas, RAII, and threads. We'll read a
file, split it into chunks, count word frequencies in parallel with
std::async (which manages threads for us), then merge the results.
#include <iostream>
#include <fstream>
#include <string>
#include <sstream>
#include <map>
#include <vector>
#include <future>
using Freq = std::map<std::string, int>;
// count words in one chunk of text (pure, easy to run on a thread)
Freq count_in(const std::string& text) {
Freq f;
std::istringstream in(text);
std::string word;
while (in >> word) f[word]++;
return f; // returned by move: no copy of the map
}
int main() {
std::ifstream in{"sample.txt"};
std::string chunk, all;
while (std::getline(in, chunk)) all += chunk + " ";
// split roughly in half and count each half on its own thread
auto mid = all.size() / 2;
std::string a = all.substr(0, mid);
std::string b = all.substr(mid);
auto fa = std::async(std::launch::async, count_in, std::cref(a));
auto fb = std::async(std::launch::async, count_in, std::cref(b));
Freq merged = fa.get(); // wait + move result
for (const auto& [w, n] : fb.get()) // structured bindings (C++17)
merged[w] += n;
for (const auto& [w, n] : merged)
std::cout << w << ": " << n << "\n";
return 0;
}
Read it back: std::async spawns threads and hands you a
std::future; .get() waits for the result and moves it out (no
copying the map). std::cref passes the string by const reference
into the async task so we don't clone the whole text. That's advanced C++ in about thirty
lines, generic helpers, concurrency, moves, and RAII all cooperating.
09Where to go next
You've now seen the full arc: from "hello, world" to templates, the algorithm library, lambdas, move semantics, the rule of zero, and real threads. These are the tools professional C++ is built from.
- Keep the standard library central. Templates + algorithms + containers solve most problems with less code and fewer bugs than hand-rolled loops and raw memory.3
- Think in ownership. "Who owns this resource, and when is it freed?" , if the answer is a smart pointer or a container, you're following the rule of zero and sleeping well at night.13
-
Be careful with threads. Shared mutable state is the hardest part of
the language; prefer
std::asyncand atomics over manual locking until you have a reason not to.3 - Read the sources. cppreference.com for the exact signature; the C++ Core Guidelines for "what should I do?"; learncpp.com for a patient, example-driven tour of every feature above.2317