| name | cpp-modern-features |
| user-invocable | false |
| description | Use when modern C++ features from C++11/14/17/20 including auto, lambdas, range-based loops, structured bindings, and concepts. |
| allowed-tools | ["Read","Write","Edit","Grep","Glob","Bash"] |
Modern C++ Features
Modern C++ (C++11 and beyond) introduced significant improvements that make
C++ more expressive, safer, and easier to use. This skill covers essential
modern features including type inference, lambda expressions, range-based
loops, smart initialization, and the latest C++20 additions.
Auto Type Inference
The auto keyword enables automatic type deduction, reducing verbosity while
maintaining type safety.
#include <iostream>
#include <vector>
#include <map>
#include <string>
void auto_examples() {
auto x = 42;
auto pi = 3.14159;
auto name = "Alice";
auto message = std::string("Hello");
std::vector<int> numbers = {1, 2, 3, 4, 5};
for (std::vector<int>::iterator it = numbers.begin();
it != numbers.end(); ++it) {
std::cout << *it << " ";
}
for (auto it = numbers.begin(); it != numbers.end(); ++it) {
std::cout << *it << " ";
}
std::map<std::string, std::vector<int>> data;
auto it = data.find("key");
auto multiply = [](int a, int b) { return a * b; };
std::map<std::string, int> scores = {{"Alice", 95}, {"Bob", 87}};
for (const auto& [name, score] : scores) {
std::cout << name << ": " << score << "\n";
}
}
Lambda Expressions
Lambdas provide inline anonymous functions, essential for modern C++
algorithms and callbacks.
#include <algorithm>
#include <vector>
#include <functional>
#include <iostream>
void lambda_examples() {
std::vector<int> numbers = {5, 2, 8, 1, 9, 3};
auto print = [](int n) { std::cout << n << " "; };
std::for_each(numbers.begin(), numbers.end(), print);
int threshold = 5;
auto above_threshold = [threshold](int n) { return n > threshold; };
auto sum_above = [=]() {
int sum = 0;
for (int n : numbers) {
if (n > threshold) sum += n;
}
return sum;
};
int count = 0;
auto count_above = [&count, threshold](int n) {
if (n > threshold) count++;
};
std::for_each(numbers.begin(), numbers.end(), count_above);
auto generic_print = [](const auto& item) {
std::cout << item << " ";
};
std::sort(numbers.begin(), numbers.end(),
[](int a, int b) { return a > b; });
auto counter = [count = 0]() mutable {
return ++count;
};
std::cout << counter() << "\n";
std::cout << counter() << "\n";
}
std::function<int(int)> make_multiplier(int factor) {
return [factor](int n) { return n * factor; };
}
Range-Based For Loops
Range-based for loops provide clean, safe iteration over containers and
ranges.
#include <vector>
#include <map>
#include <string>
#include <iostream>
void range_based_loops() {
std::vector<int> numbers = {1, 2, 3, 4, 5};
for (int n : numbers) {
std::cout << n << " ";
}
for (int& n : numbers) {
n *= 2;
}
std::vector<std::string> names = {"Alice", "Bob", "Charlie"};
for (const auto& name : names) {
std::cout << name << "\n";
}
std::map<std::string, int> ages = {
{"Alice", 30},
{"Bob", 25},
{"Charlie", 35}
};
for (const auto& [name, age] : ages) {
std::cout << name << " is " << age << " years old\n";
}
for (std::vector<int> temp = {1, 2, 3}; auto n : temp) {
std::cout << n << " ";
}
}
class Range {
int start_, end_;
public:
Range(int start, int end) : start_(start), end_(end) {}
struct Iterator {
int current;
Iterator(int val) : current(val) {}
int operator*() const { return current; }
Iterator& operator++() { ++current; return *this; }
bool operator!=(const Iterator& other) const {
return current != other.current;
}
};
Iterator begin() const { return Iterator(start_); }
Iterator end() const { return Iterator(end_); }
};
void use_custom_range() {
for (int i : Range(0, 10)) {
std::cout << i << " ";
}
}
Uniform Initialization
Uniform initialization using braces provides consistent syntax and prevents
narrowing conversions.
#include <vector>
#include <string>
#include <map>
struct Point {
int x, y;
};
void uniform_initialization() {
int a{42};
double pi{3.14159};
std::vector<int> numbers{1, 2, 3, 4, 5};
std::map<std::string, int> ages{
{"Alice", 30},
{"Bob", 25}
};
Point p{10, 20};
int x = 3.14;
int zero{};
std::string empty{};
auto get_numbers = []() { return std::vector<int>{1, 2, 3}; };
}
class Widget {
public:
Widget() = default;
Widget(int x) {}
};
void vexing_parse() {
Widget w{};
Widget w2{10};
}
Move Semantics and Rvalue References
Move semantics enable efficient transfer of resources without copying,
crucial for performance.
#include <vector>
#include <string>
#include <utility>
#include <iostream>
class Buffer {
size_t size_;
int* data_;
public:
Buffer(size_t size) : size_(size), data_(new int[size]) {
std::cout << "Constructor\n";
}
Buffer(const Buffer& other)
: size_(other.size_), data_(new int[other.size_]) {
std::copy(other.data_, other.data_ + size_, data_);
std::cout << "Copy constructor\n";
}
Buffer(Buffer&& other) noexcept
: size_(other.size_), data_(other.data_) {
other.size_ = 0;
other.data_ = nullptr;
std::cout << "Move constructor\n";
}
Buffer& operator=(const Buffer& other) {
if (this != &other) {
delete[] data_;
size_ = other.size_;
data_ = new int[size_];
std::copy(other.data_, other.data_ + size_, data_);
std::cout << "Copy assignment\n";
}
return *this;
}
Buffer& operator=(Buffer&& other) noexcept {
if (this != &other) {
delete[] data_;
size_ = other.size_;
data_ = other.data_;
other.size_ = 0;
other.data_ = nullptr;
std::cout << "Move assignment\n";
}
return *this;
}
~Buffer() { delete[] data_; }
};
void move_semantics_example() {
Buffer b1(100);
Buffer b2 = std::move(b1);
std::vector<Buffer> buffers;
buffers.push_back(Buffer(50));
auto make_buffer = [](auto&&... args) {
return Buffer(std::forward<decltype(args)>(args)...);
};
}
Variadic Templates
Variadic templates enable functions and classes that accept any number of
arguments.
#include <iostream>
#include <string>
void print() {
std::cout << "\n";
}
template<typename T, typename... Args>
void print(T first, Args... rest) {
std::cout << first << " ";
print(rest...);
}
template<typename... Args>
auto sum(Args... args) {
return (args + ...);
}
template<typename... Args>
auto sum_with_init(Args... args) {
return (args + ... + 0);
}
template<typename T, typename... Args>
std::unique_ptr<T> make_unique_custom(Args&&... args) {
return std::unique_ptr<T>(new T(std::forward<Args>(args)...));
}
void variadic_examples() {
print(1, 2.5, "hello", std::string("world"));
auto total = sum(1, 2, 3, 4, 5);
auto all_true = [](auto... args) {
return (args && ...);
};
auto any_true = [](auto... args) {
return (args || ...);
};
}
Structured Bindings (C++17)
Structured bindings decompose objects into their constituent parts,
improving code readability.
#include <tuple>
#include <map>
#include <string>
#include <array>
struct Person {
std::string name;
int age;
double salary;
};
std::tuple<int, std::string, double> get_employee() {
return {42, "Alice", 75000.0};
}
void structured_bindings() {
auto [id, name, salary] = get_employee();
std::pair<int, std::string> p{1, "one"};
auto [num, text] = p;
Person person{"Bob", 30, 80000.0};
auto [pname, page, psalary] = person;
std::array<int, 3> arr{1, 2, 3};
auto [a, b, c] = arr;
std::map<std::string, int> scores{{"Alice", 95}, {"Bob", 87}};
for (const auto& [name, score] : scores) {
std::cout << name << ": " << score << "\n";
}
auto& [rname, rage, rsalary] = person;
rage = 31;
}
Concepts (C++20)
Concepts constrain template parameters, providing better error messages and
clearer interfaces.
#include <concepts>
#include <iostream>
#include <vector>
template<typename T>
concept Numeric = std::integral<T> || std::floating_point<T>;
template<Numeric T>
T add(T a, T b) {
return a + b;
}
template<typename T>
concept Printable = requires(T t) {
{ std::cout << t } -> std::convertible_to<std::ostream&>;
};
template<Printable T>
void print(const T& value) {
std::cout << value << "\n";
}
template<typename T>
concept Range = requires(T r) {
r.begin();
r.end();
};
template<Range R>
void print_range(const R& range) {
for (const auto& item : range) {
std::cout << item << " ";
}
std::cout << "\n";
}
template<typename T>
concept Container = requires(T c) {
typename T::value_type;
typename T::iterator;
{ c.begin() } -> std::same_as<typename T::iterator>;
{ c.end() } -> std::same_as<typename T::iterator>;
{ c.size() } -> std::convertible_to<std::size_t>;
};
template<Container C>
void process_container(const C& container) {
std::cout << "Size: " << container.size() << "\n";
}
void concepts_example() {
auto result = add(5, 10);
auto dresult = add(5.5, 2.3);
print(42);
print("Hello");
std::vector<int> vec{1, 2, 3};
print_range(vec);
process_container(vec);
}
Ranges Library (C++20)
The ranges library provides composable algorithms and views for working with
sequences.
#include <ranges>
#include <vector>
#include <iostream>
#include <algorithm>
void ranges_examples() {
std::vector<int> numbers{1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
auto even = [](int n) { return n % 2 == 0; };
auto square = [](int n) { return n * n; };
auto result = numbers
| std::views::filter(even)
| std::views::transform(square)
| std::views::take(3);
for (int n : result) {
std::cout << n << " ";
}
std::cout << "\n";
std::ranges::sort(numbers, std::greater{});
struct Person {
std::string name;
int age;
};
std::vector<Person> people{
{"Alice", 30},
{"Bob", 25},
{"Charlie", 35}
};
auto it = std::ranges::find(people, "Bob", &Person::name);
for (int i : std::views::iota(1, 6)) {
std::cout << i << " ";
}
std::cout << "\n";
std::string text = "one,two,three";
for (auto word : text | std::views::split(',')) {
for (char c : word) {
std::cout << c;
}
std::cout << " ";
}
}
Best Practices
- Use
auto for complex types and iterators but keep simple types explicit
- Prefer lambdas over function objects for inline operations and callbacks
- Use range-based for loops instead of manual iterator manipulation
- Initialize variables with
{} to prevent narrowing conversions
- Implement move constructors and assignments for resource-owning classes
- Use
std::move when transferring ownership, not for general optimization
- Prefer structured bindings over
std::get<>() for tuples and pairs
- Use concepts to constrain templates and improve error messages
- Leverage ranges for composable, lazy operations on sequences
- Use
const auto& for range-based loops with large objects
Common Pitfalls
- Overusing
auto making code less readable when types provide clarity
- Capturing by reference in lambdas that outlive their captures
- Using
std::move on const objects, which disables move semantics
- Forgetting
noexcept on move operations, preventing optimizations
- Modifying containers while iterating with range-based for loops
- Dangling references from structured bindings of temporary objects
- Using fold expressions without considering operator precedence
- Assuming ranges views create copies instead of providing lazy views
- Moving from objects that will be used again later
- Not marking move constructors and assignments as
noexcept
When to Use Modern C++ Features
Use modern C++ features when you need:
- Cleaner, more expressive code with less boilerplate
- Better type safety with concepts and structured bindings
- Improved performance through move semantics
- Functional programming patterns with lambdas and ranges
- Generic programming with less template complexity
- Safer resource management with smart pointers
- Code that's easier to maintain and refactor
- Better compiler error messages with concepts
- Lazy evaluation and composition with ranges
- Migration from older C++ codebases to modern standards
Resources