Modern C++ Features (C++17/20/23)¶
Key features from recent standards that change how C++ is written. Focus on the most impactful additions.
Key Facts - C++17¶
- Structured bindings:
auto [key, value] = pair; std::optional<T>,std::variant<Ts...>,std::anyif constexpr- compile-time branching in templates- Class Template Argument Deduction (CTAD):
std::vector v{1,2,3}; std::string_view- non-owning string reference<filesystem>- portable filesystem operations- Fold expressions:
(args + ...) - Nested namespaces:
namespace A::B::C {} [[nodiscard]],[[maybe_unused]],[[fallthrough]]attributesstd::invoke- uniform callable invocation
Key Facts - C++20¶
- Concepts and constraints
- Ranges and views (
<ranges>) - Coroutines (
co_await,co_yield,co_return) - Modules (
import,export module) std::format- type-safe formatting<=>spaceship operatorstd::span<T>- non-owning contiguous viewstd::jthread- auto-joining thread with stop tokenconsteval,constinitrequiresexpressions and clauses
Key Facts - C++23¶
std::expected<T,E>- value-or-errorstd::print/std::println- formatted outputstd::mdspan- multidimensional array view- Deducing
this- explicit object parameter std::generator- synchronous coroutine generatorif consteval- check if in constant evaluationstd::flat_map/std::flat_set- sorted vector-based containersstd::stacktrace- programmatic stack traces
Patterns¶
Structured Bindings (C++17)¶
// With pairs/tuples
auto [min_it, max_it] = std::minmax_element(v.begin(), v.end());
// With maps
std::map<std::string, int> scores;
for (const auto& [name, score] : scores) {
std::cout << name << ": " << score << '\n';
}
// With custom structs (public members or structured binding support)
struct Result { bool ok; std::string message; };
auto [ok, msg] = get_result();
if (ok) process(msg);
// With arrays
int arr[] = {1, 2, 3};
auto [a, b, c] = arr;
std::variant and std::visit (C++17)¶
using Value = std::variant<int, double, std::string>;
Value v = 42;
v = "hello"s;
// Visit with overloaded lambdas
template<class... Ts> struct overloaded : Ts... { using Ts::operator()...; };
auto result = std::visit(overloaded{
[](int i) { return std::to_string(i); },
[](double d) { return std::to_string(d); },
[](const std::string& s) { return s; }
}, v);
// Check which type is active
if (std::holds_alternative<int>(v)) {
int i = std::get<int>(v);
}
// Safe get
if (auto* p = std::get_if<std::string>(&v)) {
process(*p);
}
Ranges and Views (C++20)¶
#include <ranges>
namespace rv = std::views;
std::vector<int> nums = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
// Pipeline
auto evens_squared = nums
| rv::filter([](int n) { return n % 2 == 0; })
| rv::transform([](int n) { return n * n; });
// Lazy evaluation - nothing computed until iterated
for (int n : evens_squared) { std::cout << n << ' '; } // 4 16 36 64 100
// Range factories
for (int i : rv::iota(1, 11)) { /* 1..10 */ }
for (int i : rv::iota(0) | rv::take(5)) { /* 0..4 */ }
// String processing
std::string csv = "a,b,c,d";
for (auto word : csv | rv::split(',')) {
// process each word
}
std::span (C++20)¶
#include <span>
// Non-owning view of contiguous data - replaces (T*, size_t) pairs
void process(std::span<const int> data) {
for (int x : data) { /* ... */ }
auto first3 = data.subspan(0, 3);
auto last2 = data.last(2);
}
// Works with any contiguous container
std::vector<int> vec = {1, 2, 3};
std::array<int, 5> arr = {1, 2, 3, 4, 5};
int c_arr[] = {1, 2, 3, 4};
process(vec);
process(arr);
process(c_arr);
process({vec.data() + 1, 3}); // subview
Deducing this (C++23)¶
struct Widget {
// Single implementation for const and non-const
template<typename Self>
auto&& value(this Self&& self) {
return std::forward<Self>(self).value_;
}
// Recursive lambda (see also lambda-expressions)
auto get_children(this const Widget& self) -> std::vector<Widget> {
// can recurse: self.get_children()
}
private:
int value_;
};
Modules (C++20)¶
// mylib.cppm (module interface)
export module mylib;
export class Widget {
public:
Widget(int val);
int value() const;
private:
int val_;
};
export int compute(int x);
// mylib.cpp (module implementation)
module mylib;
Widget::Widget(int val) : val_(val) {}
int Widget::value() const { return val_; }
int compute(int x) { return x * x; }
// main.cpp
import mylib;
int main() {
Widget w(42);
return compute(w.value());
}
Gotchas¶
- Issue:
std::variantdefault-constructs first alternative - fails if first type has no default ctor -> Fix: Put a default-constructible type first, or usestd::monostateas first type - Issue: Ranges views hold references to source - dangling if source is temporary -> Fix: Materialize views to containers before source destruction. Don't return views to local data.
- Issue: Modules support varies across compilers (partial in GCC/Clang as of 2025) -> Fix: Check compiler support matrix. Use headers as fallback with
#includewrapper. - Issue:
std::spandoesn't own data - same dangling risks as pointers -> Fix: Ensure source outlives the span. Never return span to local container.