51. Modern C++ Features
Definition
Modern C++ refers to the C++11 standard and beyond (C++14, C++17, C++20, C++23). Modern features improve productivity, safety, and performance compared to traditional C++. This article covers the most impactful features that every modern C++ developer must understand.
auto — Type Deduction
auto lets the compiler deduce a variable's type automatically, eliminating excessive type declarations.
Code Example
#include <iostream>
#include <vector>
#include <map>
#include <string>
using namespace std;
int main() {
// auto deduces the type from the initializer
auto x = 42; // int
auto y = 3.14; // double
auto name = "Mike"; // const char*
auto grade = 42.0f; // float
// Useful for complex types
vector<int> numbers = {1, 2, 3, 4, 5};
auto it = numbers.begin(); // vector<int>::iterator
map<string, int> score = {{"Mike", 90}};
auto pair_it = score.begin(); // map<string,int>::iterator
// Lambda without writing parameter types
auto add = [](int a, int b) { return a + b; };
cout << add(3, 4) << endl;
// In a range-based for
for (const auto& val : numbers) {
cout << val << " ";
}
cout << endl;
return 0;
}
Code Explanation
autois not a type — the compiler replaces it with the actual type at compile time.- It doesn't change performance — the type is still known at compile time.
- It helps avoid type typos and makes code cleaner.
decltype — Declaration Type
decltype obtains the type of an expression without evaluating it.
Code Example
#include <iostream>
using namespace std;
int main() {
int x = 10;
decltype(x) y = 20; // y is int
decltype(x + 0.5) z; // z is double
// Useful for complex return types
auto add = [](int a, double b) -> decltype(a + b) {
return a + b;
};
cout << "x: " << x << ", y: " << y << endl;
cout << "Add: " << add(3, 4.5) << endl;
return 0;
}
Code Explanation
decltype(expr)returns the type ofexprwithout evaluating it.- Useful for generic programming and forward declarations.
nullptr — Null Pointer
nullptr is a type-safe pointer literal, replacing NULL or 0 as a null designator.
Code Example
#include <iostream>
using namespace std;
void function(int x) {
cout << "Versi int: " << x << endl;
}
void function(int* p) {
if (p != nullptr) {
cout << "Versi pointer: " << *p << endl;
} else {
cout << "Pointer null" << endl;
}
}
int main() {
function(42); // calls the int version
function(nullptr); // calls the pointer version
// function(NULL); // AMBIGUOUS — could call both versions
int* ptr = nullptr;
if (ptr == nullptr) {
cout << "Null pointer" << endl;
}
return 0;
}
Code Explanation
nullptrhas the typestd::nullptr_t, compatible with all pointer types.- It eliminates overload ambiguity between int and pointer.
Range-Based For
Range-based for makes iterating over all elements of a container easy.
Code Example
#include <iostream>
#include <vector>
#include <map>
#include <set>
using namespace std;
int main() {
// C-style array
int arr[] = {1, 2, 3, 4, 5};
for (int val : arr) {
cout << val << " ";
}
cout << endl;
// Vector
vector<string> buah = {"Apel", "Mangga", "Jeruk"};
for (const auto& b : buah) {
cout << b << " ";
}
cout << endl;
// Map
map<string, int> age = {{"Mike", 25}, {"John", 30}};
for (const auto& [name, age] : age) {
cout << name << ": " << age << " years" << endl;
}
// Set
set<int> numbers = {10, 20, 30};
for (auto val : numbers) { // copy because a set can't be modified via const ref
cout << val << " ";
}
cout << endl;
return 0;
}
Code Explanation
const auto&to read without copying and without modifying.auto&to modify elements directly.auto(copy) for small types that are faster to copy.
Structured Bindings (C++17)
Structured bindings allow destructuring data directly in a variable declaration.
Code Example
#include <iostream>
#include <tuple>
#include <map>
#include <array>
using namespace std;
int main() {
// Destructuring a pair
pair<string, int> student = {"Mike", 90};
auto [name, score] = student;
cout << name << ": " << score << endl;
// Destructuring a tuple
tuple<double, double, double> coordinates = {1.0, 2.0, 3.0};
auto [x, y, z] = coordinates;
cout << "Coordinates: (" << x << ", " << y << ", " << z << ")" << endl;
// Destructuring an array
array<int, 3> data = {10, 20, 30};
auto [a, b, c] = data;
cout << "Array: " << a << ", " << b << ", " << c << endl;
// Destructuring a map
map<string, double> grade = {{"Mathematics", 90.5}, {"Physics", 85.0}};
for (const auto& [subject, score] : grade) {
cout << subject << ": " << score << endl;
}
// Struct
struct Point { double x, y, z; };
Point p = {1.0, 2.0, 3.0};
auto [px, py, pz] = p;
cout << "Point: (" << px << ", " << py << ", " << pz << ")" << endl;
return 0;
}
Code Explanation
- Works with pairs, tuples, arrays, structs, and initializer lists.
- Makes code more expressive and eliminates
.first,.second, andget<>().
if/switch with Initializers (C++17)
C++17 allows declaring variables inside the condition of if/switch.
Code Example
#include <iostream>
#include <map>
#include <string>
using namespace std;
int main() {
map<string, int> inventaris = {
{"apple", 50}, {"orange", 30}, {"mango", 0}
};
string item = "orange";
// if with initializer
if (auto it = inventaris.find(item); it != inventaris.end()) {
cout << item << ": " << it->second << " stock" << endl;
} else {
cout << item << " not found" << endl;
}
// switch with initializer (simple example)
int grade = 85;
switch (auto grade = grade / 10; grade) {
case 9: cout << "Grade: A" << endl; break;
case 8: cout << "Grade: B" << endl; break;
case 7: cout << "Grade: C" << endl; break;
default: cout << "Grade: D" << endl; break;
}
return 0;
}
Code Explanation
- A declared variable is scoped only inside the if/switch block.
- Eliminates temporary variables in a wider scope that aren't needed.
std::optional (C++17)
optional wraps a value that may not exist, replacing null pointers or sentinel values.
Code Example
#include <iostream>
#include <optional>
#include <string>
using namespace std;
// A function that may fail
optional<int> findScore(const string& name) {
if (name == "Mike") return 90;
if (name == "John") return 85;
return nullopt; // not found
}
int main() {
auto score = findScore("Mike");
if (score.has_value()) {
cout << "Score for Mike: " << score.value() << endl;
}
// operator* to access the value
auto johnScore = findScore("John");
if (johnScore) {
cout << "Score John: " << *johnScore << endl;
}
// Default value with value_or
auto annaScore = findScore("Anna");
cout << "Score Anna: " << annaScore.value_or(0) << endl;
return 0;
}
Code Explanation
optional<T>can hold either a value or nullopt.has_value()or thebooloperator checks for the presence of a value.value()oroperator*access the value — make sure it has a value first.value_or(default)returns the value or a default if empty.
std::variant (C++17)
variant is a type-safe tagged union — it can hold one of several predefined types.
Code Example
#include <iostream>
#include <variant>
#include <string>
using namespace std;
int main() {
variant<int, string, double> data;
data = 42;
cout << "Int: " << get<int>(data) << endl;
data = "Hello";
cout << "String: " << get<string>(data) << endl;
data = 3.14;
cout << "Double: " << get<double>(data) << endl;
// Check the stored type
if (holds_alternative<int>(data)) {
cout << "Saved as int" << endl;
}
// Visitor pattern
auto display = [](const auto& val) {
cout << "Value: " << val << endl;
};
visit(display, data);
return 0;
}
Code Explanation
get<T>(v)retrieves the value — throwsbad_variant_accessif the type is wrong.holds_alternative<T>(v)checks the stored type.visit()applies a visitor to the value — the safest way to access it.
std::any (C++17)
any can hold a value of any type without specifying the type at declaration.
Code Example
#include <iostream>
#include <any>
#include <string>
using namespace std;
int main() {
any data;
data = 42;
cout << "Int: " << any_cast<int>(data) << endl;
data = string("Hello");
cout << "String: " << any_cast<string>(data) << endl;
data = 3.14;
cout << "Double: " << any_cast<double>(data) << endl;
// Check the type
cout << "Type: " << data.type().name() << endl;
// Safe with any_cast
try {
int val = any_cast<int>(data); // data holds a double
} catch (const bad_any_cast& e) {
cout << "Cast failed: " << e.what() << endl;
}
return 0;
}
Code Explanation
any_cast<T>()converts an any to type T — with a runtime check.- Less efficient than variant because of boxing/unboxing.
- Use variant when the types are known at compile time.
Fold Expressions (C++17)
Fold expressions allow operations on parameter packs without recursive templates.
Code Example
#include <iostream>
using namespace std;
// Sum all arguments
template<typename... Args>
auto sum(Args... args) {
return (args + ...); // unary right fold
}
// Print all arguments
template<typename... Args>
void print(Args... args) {
(cout << ... << args) << endl; // binary left fold
}
// Check all are true
template<typename... Args>
bool allTrue(Args... args) {
return (... && args); // unary left fold
}
int main() {
cout << "Sum: " << sum(1, 2, 3, 4, 5) << endl;
print("Mike", " ", 90, " ", 3.5);
cout << "All true: " << allTrue(true, true, true) << endl;
cout << "All true: " << allTrue(true, false, true) << endl;
return 0;
}
Code Explanation
(args op ...)— unary fold (no default).(args op ... op init)— binary fold with an initial value.- Allowed operators:
+,*,&&,||,,.
C++20: A Brief Overview
C++20 brings major features: Concepts, Ranges, and Modules.
Concepts
#include <iostream>
#include <concepts>
using namespace std;
// Defining a concept
template<typename T>
concept Numeric = integral<T> || floating_point<T>;
// Using a concept
template<Numeric T>
T add(T a, T b) {
return a + b;
}
int main() {
cout << add(3, 4) << endl; // OK
cout << add(3.5, 2.5) << endl; // OK
// add("a", "b"); // Error: doesn't satisfy the Numeric concept
return 0;
}
Code Explanation
- Concepts provide constraints on template parameters.
- Error messages are clearer than with SFINAE.
- They replace
enable_ifin many cases.
Tips for Using Modern Features
#include <iostream>
#include <vector>
#include <algorithm>
#include <optional>
#include <string>
using namespace std;
// Using all modern features together
struct Student {
string name;
optional<double> grade;
};
int main() {
vector<Student> list = {
{"Mike", 90.0}, {"John", nullopt}, {"Anna", 85.0}
};
// Structured bindings + range-based for + optional
for (const auto& [name, grade] : list) {
if (grade) {
cout << name << ": " << *grade << endl;
} else {
cout << name << ": no grade yet" << endl;
}
}
// Lambda + auto
auto compare = [](const Student& a, const Student& b) {
double va = a.grade.value_or(0);
double vb = b.grade.value_or(0);
return va > vb;
};
sort(list.begin(), list.end(), compare);
cout << "\nAfter sorting:" << endl;
for (const auto& [name, grade] : list) {
cout << name << ": " << grade.value_or(0) << endl;
}
return 0;
}
Summary
Modern C++ features improve clarity, safety, and expressiveness of code. auto eliminates verbosity, optional replaces null, variant replaces unions, and structured bindings make destructuring easier. Concepts in C++20 strengthen template constraints. Mastering these features is the key to writing modern, maintainable C++.