C++20 is the biggest update to C++ since C++11. It's like upgrading from a flip phone to a smartphone - suddenly you have powerful new tools that make complex tasks simple and elegant.
Concepts are like job requirements for templates. Instead of accepting any type and hoping it works, you can specify exactly what capabilities a type must have.
// Define a concept
template<typename T>
concept Numeric = std::is_arithmetic_v<T>;
// Use concept to constrain template
template<Numeric T>
T add(T a, T b) {
return a + b;
}
// Or with requires clause
template<typename T>
requires Numeric<T>
T multiply(T a, T b) {
return a * b;
}
// Usage
int result1 = add(5, 3); // OK: int is Numeric
double result2 = add(3.14, 2.71); // OK: double is Numeric
// std::string s = add("Hello", "World"); // ERROR: string is not Numeric
// The error message is clear:
// "no matching function for call to 'add'
// template argument deduction/substitution failed:
// constraints not satisfied
// concept 'Numeric' was not satisfied"
// Concept requiring specific operations
template<typename T>
concept Printable = requires(T t) {
std::cout << t; // Must support stream output
};
// Concept with multiple requirements
template<typename T>
concept Container = requires(T t) {
typename T::value_type; // Must have value_type
typename T::size_type; // Must have size_type
typename T::iterator; // Must have iterator
{ t.size() } -> std::convertible_to<std::size_t>; // size() returns size_t
{ t.begin() } -> std::same_as<typename T::iterator>;
{ t.end() } -> std::same_as<typename T::iterator>;
};
// Combining concepts
template<typename T>
concept SortableContainer = Container<T> && requires(T t) {
std::sort(t.begin(), t.end()); // Elements must be sortable
};
// Real-world example: Generic algorithm with clear requirements
template<SortableContainer C>
void sortAndPrint(C& container) {
std::sort(container.begin(), container.end());
for (const auto& item : container) {
std::cout << item << " ";
}
std::cout << "\n";
}
Ranges transform how we work with sequences. Instead of iterator pairs, we work with whole ranges and compose operations like building with LEGO blocks.
#include <ranges>
#include <vector>
#include <iostream>
// Traditional approach
std::vector<int> numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
std::vector<int> result;
// Old way: Multiple passes, temporary vectors
for (int n : numbers) {
if (n % 2 == 0) {
result.push_back(n * n);
}
}
// New way: Ranges with pipeline syntax
auto result_range = numbers
| std::views::filter([](int n) { return n % 2 == 0; }) // Keep even numbers
| std::views::transform([](int n) { return n * n; }); // Square them
// Lazy evaluation - nothing computed yet!
for (int n : result_range) {
std::cout << n << " "; // 4 16 36 64 100 - computed on demand
}
// Common range adaptors
namespace views = std::views; // Convenience alias
std::vector<int> data = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
// take: First n elements
auto first_three = data | views::take(3); // 1, 2, 3
// drop: Skip first n elements
auto skip_three = data | views::drop(3); // 4, 5, 6, 7, 8, 9, 10
// reverse: Reverse order
auto backwards = data | views::reverse; // 10, 9, 8, ..., 1
// keys/values: For maps
std::map<std::string, int> scores = {{"Alice", 95}, {"Bob", 87}, {"Carol", 92}};
auto names = scores | views::keys; // "Alice", "Bob", "Carol"
auto grades = scores | views::values; // 95, 87, 92
// Complex pipeline
auto processed = data
| views::filter([](int n) { return n > 3; }) // Greater than 3
| views::transform([](int n) { return n * 2; }) // Double it
| views::take(4) // First 4 results
| views::reverse; // In reverse order
// Result: 20, 18, 16, 14
// Custom range adaptor for sliding window
template<std::ranges::forward_range R>
class sliding_window_view : public std::ranges::view_interface<sliding_window_view<R>> {
R base_;
std::size_t window_size_;
public:
sliding_window_view(R base, std::size_t window_size)
: base_(std::move(base)), window_size_(window_size) {}
auto begin() const {
return sliding_window_iterator{std::ranges::begin(base_),
std::ranges::end(base_),
window_size_};
}
auto end() const {
return sliding_window_sentinel{};
}
};
// Usage
std::vector<int> data = {1, 2, 3, 4, 5};
for (auto window : sliding_window_view(data, 3)) {
// Process each window: [1,2,3], [2,3,4], [3,4,5]
}
Coroutines are functions that can pause and resume execution. Think of them like a TV series - you can pause mid-episode and continue later from exactly where you left off.
// Simple generator for infinite sequence
template<typename T>
struct Generator {
struct promise_type {
T current_value;
Generator get_return_object() {
return Generator{handle_type::from_promise(*this)};
}
std::suspend_always initial_suspend() { return {}; }
std::suspend_always final_suspend() noexcept { return {}; }
void unhandled_exception() { std::terminate(); }
std::suspend_always yield_value(T value) {
current_value = value;
return {};
}
void return_void() {}
};
using handle_type = std::coroutine_handle<promise_type>;
handle_type coro;
explicit Generator(handle_type h) : coro(h) {}
~Generator() { if (coro) coro.destroy(); }
// Make it range-based for loop compatible
struct iterator {
handle_type coro;
bool done;
iterator& operator++() {
coro.resume();
done = coro.done();
return *this;
}
T operator*() const { return coro.promise().current_value; }
bool operator!=(const iterator&) const { return !done; }
};
iterator begin() {
coro.resume();
return {coro, coro.done()};
}
iterator end() { return {coro, true}; }
};
// Fibonacci generator
Generator<int> fibonacci() {
int a = 0, b = 1;
while (true) {
co_yield a;
auto next = a + b;
a = b;
b = next;
}
}
// Usage
for (auto fib : fibonacci()) {
if (fib > 1000) break;
std::cout << fib << " ";
}
// Output: 0 1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987
// Task coroutine for async operations
template<typename T>
struct Task {
struct promise_type {
std::optional<T> value;
std::exception_ptr exception;
Task get_return_object() {
return Task{handle_type::from_promise(*this)};
}
std::suspend_never initial_suspend() { return {}; }
std::suspend_always final_suspend() noexcept { return {}; }
void return_value(T val) { value = std::move(val); }
void unhandled_exception() { exception = std::current_exception(); }
};
using handle_type = std::coroutine_handle<promise_type>;
handle_type coro;
// Awaitable interface
bool await_ready() { return coro.done(); }
void await_suspend(std::coroutine_handle<> h) { /* ... */ }
T await_resume() {
if (coro.promise().exception)
std::rethrow_exception(coro.promise().exception);
return *coro.promise().value;
}
};
// Async function using coroutines
Task<std::string> fetchData(const std::string& url) {
// Simulate async HTTP request
auto response = co_await httpGet(url);
auto json = co_await parseJson(response);
co_return json["data"].asString();
}
// Usage with multiple async operations
Task<void> processMultipleUrls() {
std::vector<std::string> urls = {
"https://api1.example.com",
"https://api2.example.com",
"https://api3.example.com"
};
for (const auto& url : urls) {
try {
auto data = co_await fetchData(url);
std::cout << "Received: " << data << "\n";
} catch (const std::exception& e) {
std::cout << "Error: " << e.what() << "\n";
}
}
}
Let's combine concepts, ranges, and coroutines to create powerful, expressive code.
// Concept for async data sources
template<typename T>
concept AsyncDataSource = requires(T t) {
{ t.fetchNext() } -> std::same_as<Task<std::optional<typename T::value_type>>>;
};
// Coroutine that processes data with ranges
template<AsyncDataSource Source>
Task<std::vector<int>> processAsyncData(Source& source) {
std::vector<int> results;
// Fetch data asynchronously
while (auto data = co_await source.fetchNext()) {
// Use ranges to process
auto processed = *data
| std::views::filter([](int n) { return n > 0; })
| std::views::transform([](int n) { return n * n; })
| std::views::take(10);
results.insert(results.end(), processed.begin(), processed.end());
}
co_return results;
}
// Generator with ranges
Generator<std::string> wordGenerator(const std::string& text) {
auto words = text
| std::views::split(' ')
| std::views::transform([](auto&& word) {
return std::string(word.begin(), word.end());
});
for (const auto& word : words) {
co_yield word;
}
}
// Define a concept for types that can be serialized
template<typename T>
concept Serializable = requires(T t, std::ostream& os) {
// TODO: Add requirements
// - Must have serialize method
// - Must have deserialize static method
// - Must be default constructible
};
// Create a generic save/load function
template<Serializable T>
void saveToFile(const T& obj, const std::string& filename) {
// TODO: Implement
}
template<Serializable T>
T loadFromFile(const std::string& filename) {
// TODO: Implement
}
// Create a custom range view that groups consecutive equal elements
// Input: [1, 1, 2, 2, 2, 3, 1, 1]
// Output: [[1, 1], [2, 2, 2], [3], [1, 1]]
template<std::ranges::input_range R>
class group_consecutive_view {
// TODO: Implement the view
};
// Create the adaptor
inline constexpr auto group_consecutive = // TODO
// Usage:
std::vector<int> data = {1, 1, 2, 2, 2, 3, 1, 1};
for (auto group : data | group_consecutive) {
// Process each group
}
// Create a coroutine that:
// 1. Reads lines from multiple files asynchronously
// 2. Filters lines containing a keyword
// 3. Transforms them to uppercase
// 4. Yields results as they become available
Generator<std::string> asyncGrep(
const std::vector<std::string>& filenames,
const std::string& keyword) {
// TODO: Implement using coroutines and ranges
// Hint: Use co_await for async file reading
// Use ranges for filtering and transformation
}
C++20 represents a paradigm shift in how we write C++ code:
These features work together synergistically. Concepts constrain your templates, ranges compose your algorithms, and coroutines handle your async operations. The result is C++ code that's more readable, maintainable, and efficient than ever before.