Modern C++ (C++11 and later) transformed the language: auto, range-for, and lambdas removed boilerplate; move semantics eliminated unnecessary copies; smart pointers replaced raw new/delete; constexpr moved computation to compile time; C++17 added structured bindings and std::optional; C++20 added concepts, ranges, and coroutines.
C++03: std::vector<std::pair<std::string, int>>::iterator it = v.begin(); — eight tokens to say "iterator". C++11: auto it = v.begin(); — same meaning, zero noise. This is the spirit of modern C++: the power stays, the ceremony goes.
C++11 essentials: auto, lambdas, range-for
#include <vector>
#include <algorithm>
#include <iostream>
#include <string>
int main() {
std::vector<std::string> words = {"banana", "apple", "cherry", "date"};
// auto: type deduced from initialiser — never wrong, always readable
auto count = words.size(); // size_t, not unsigned long or whatever
std::cout << "Count: " << count << "\n";
// Range-for: iterate without iterator boilerplate
for (const auto& word : words) { // const auto& avoids copying strings
std::cout << word << "\n";
}
// Lambda: anonymous function defined inline
// [capture](params) { body }
auto isLong = [](const std::string& s) { return s.size() > 4; };
// Lambda with capture: [=] captures all by value, [&] by reference
int minLen = 5;
auto meetsCriteria = [&minLen](const std::string& s) {
return s.size() >= static_cast<size_t>(minLen);
};
long longWords = std::count_if(words.begin(), words.end(), isLong);
std::cout << "Words longer than 4 chars: " << longWords << "\n"; // 2
// std::sort with lambda comparator
std::sort(words.begin(), words.end(),
[](const std::string& a, const std::string& b) { return a < b; });
for (const auto& w : words) std::cout << w << " "; // apple banana cherry date
std::cout << "\n";
}C++17: structured bindings and std::optional
#include <optional>
#include <map>
#include <string>
#include <iostream>
// std::optional: a value that may or may not be present
// Replaces the "return -1 on failure" or "output parameter" patterns
std::optional<int> divide(int a, int b) {
if (b == 0) return std::nullopt; // no value
return a / b; // has value
}
int main() {
// Structured bindings (C++17): unpack pairs, tuples, structs
std::map<std::string, int> scores = {{"Alice", 95}, {"Bob", 87}};
for (const auto& [name, score] : scores) { // unpacks key-value pair
std::cout << name << ": " << score << "\n";
}
// optional: check before use
auto result = divide(10, 3);
if (result) {
std::cout << "Result: " << *result << "\n"; // 3
}
auto bad = divide(5, 0);
std::cout << bad.value_or(-1) << "\n"; // -1 (fallback)
// if constexpr (C++17): compile-time conditional in templates
// if (std::is_integral_v<T>) { ... } — evaluated at compile time
}C++20: concepts and ranges
#include <concepts>
#include <ranges>
#include <vector>
#include <iostream>
// Concepts: named, readable constraints on template parameters
template<std::integral T>
T gcd(T a, T b) {
while (b) { a %= b; std::swap(a, b); }
return a;
}
int main() {
std::vector<int> v = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
// Ranges pipeline: lazy, composable, readable
auto evens_doubled = v
| std::views::filter([](int x){ return x % 2 == 0; })
| std::views::transform([](int x){ return x * 2; });
for (int x : evens_doubled) std::cout << x << " ";
std::cout << "\n"; // 4 8 12 16 20
// std::format (C++20): type-safe string formatting
// std::string msg = std::format("pi ≈ {:.4f}", 3.14159);
std::cout << gcd(48, 18) << "\n"; // 6
// gcd("hello", "world"); // COMPILE ERROR: "hello" is not integral
}Move semantics in depth
Move semantics (C++11) allow a constructor or assignment to "steal" the resources of a temporary (rvalue) instead of copying them. An rvalue reference (T&&) binds to temporaries. std::move casts a named variable to an rvalue reference — it doesn't move anything itself, it just grants permission. After a move, the source object is in a valid but unspecified state: it can be destroyed or reassigned but not used for its original purpose.
#include <string>
#include <vector>
#include <iostream>
#include <utility>
int main() {
std::string s1 = "Hello, World!";
// Copy: s1 is unchanged, s2 has its own copy of the data
std::string s2 = s1;
std::cout << "s1: " << s1 << "\n"; // still "Hello, World!"
// Move: s3 steals s1's internal buffer — O(1) vs O(n) copy
std::string s3 = std::move(s1);
std::cout << "s3: " << s3 << "\n"; // "Hello, World!"
std::cout << "s1: " << s1 << "\n"; // "" (valid but unspecified)
// emplace_back constructs in-place (avoids copy/move entirely)
std::vector<std::string> v;
v.reserve(3);
v.emplace_back("first"); // constructs string directly in vector
v.push_back("second"); // constructs temp then moves into vector
v.push_back(std::move(s2)); // moves s2 into vector — no copy
std::cout << v.size() << "\n"; // 3
}constexpr and compile-time computation
constexpr functions can execute at compile time when given compile-time inputs. C++20 expanded this significantly: std::vector and std::string are partially constexpr; consteval forces compile-time-only evaluation; constinit ensures static initialisation order safety. Compile-time computation shifts work from runtime to build time, producing programs that start faster and have fewer runtime failure modes.
auto x = 5; declares x as a compile-time-typed int — the type is deduced from the initialiser and is fixed forever. You cannot later do x = "hello". auto is type inference, not type erasure. The type is known to the compiler as precisely as if you had written int x = 5;.std::move(x) is a cast — it converts x to an rvalue reference (T&&), which grants permission to move constructors and move assignment operators to steal x's resources. The actual transfer happens in the constructor or assignment that receives the rvalue reference. Calling std::move on something that is then passed to a function that only accepts const T& silently falls back to copying.optional<T> stores the value inline (on the stack if the optional is on the stack) — no heap allocation, no null pointer. Dereferencing a nullopt optional throws std::bad_optional_access. Prefer value_or(default) for safe extraction. optional<T*> is possible but almost never the right choice — it combines two levels of optionality.The C++ standards process and ABI stability
C++ is standardised by ISO/IEC JTC1/SC22/WG21, which meets multiple times per year. The three-year release cadence (C++11, C++14, C++17, C++20, C++23) reflects a deliberate balance: enough time for features to mature, not so long that the language stagnates. The C++ committee operates on consensus — major features (modules, coroutines, contracts) go through years of proposals, revisions, and technical specifications before acceptance. ABI stability is a persistent tension: the Linux and Windows C++ ABIs have been stable for decades (GCC's libstdc++ ABI has been stable since GCC 3.4 in 2004), which prevents fixing known inefficiencies in std::string (the small-string optimisation threshold), std::regex (known to be slow), and std::list (cache-hostile). C++26 may change the ABI for selected types.
C++20 coroutines: stackless coroutines for async I/O
C++20 coroutines are stackless — they suspend and resume at co_await, co_yield, and co_return points. A coroutine's state is stored on the heap (the coroutine frame), not the stack. Unlike Python generators (which are language-integrated) or Go goroutines (runtime-scheduled), C++ coroutines are a lower-level mechanism: they define the suspension points but delegate scheduling to the caller or a custom executor. Libraries like Asio and cppcoro build higher-level async patterns on top of the coroutine primitives. The coroutine machinery is entirely zero-overhead-abstraction: a coroutine that never suspends is compiled to the same code as a regular function.
ISO/IEC 14882:2020 (C++20). iso.org/standard/79358.html. ISO/IEC 14882:2024 (C++23 published). Meyers, S. (2014). Effective Modern C++. O'Reilly. — C++11/14 features in depth. Stroustrup, B. (2018). A Tour of C++ (2nd ed.). Addison-Wesley. cppreference.com — full C++11–C++23 feature coverage.