This episode covers modern C++ features: auto, range-based loops, and structured bindings, move semantics with rvalue references, std::optional, std::variant, std::any, and coroutines, as well as modules, concepts, and constexpr improvements.

The C++ you learn from old books and the C++ the industry uses today are fundamentally different. Since C++11, the language has transformed: more concise ways of writing, safer memory management, and features that were previously impossible are now possible.
Episode 16 summarizes the most influential modern features: auto and structured bindings that make code shorter, move semantics that eliminate wasteful copies, std::optional and std::variant that express possibilities, constexpr that moves computations to compile time, as well as coroutines and modules that change how programs are structured.
auto lets the compiler deduce the type from the initializer. In modern C++, auto is the default for local variables — code stays concise without losing type safety. auto& makes a reference, and const auto& for read-only without copying:
cat > modern1.cpp <<'EOF'
#include <iostream>
#include <map>
#include <string>
int main() {
std::map<std::string, int> nilai{{"A", 90}, {"B", 85}};
for (const auto& [nama, skor] : nilai) {
std::cout << nama << ": " << skor << "\n";
}
}
EOF
g++ -std=c++20 modern1.cpp -o modern1
./modern1for (const auto& [nama, skor] : nilai) is a structured binding (C++17): a range-based loop that also unpacks key-value pairs. Before C++17, you had to write it->first and it->second manually.
A range-based loop copies elements by default. For large elements like strings or objects, use const auto& to avoid copying. That one-letter difference determines whether a program keeps allocating memory or not.
An lvalue is an expression that has an address and can have its reference taken; an rvalue is a temporary value. Move semantics transfer a resource from an object about to be discarded to a new object, instead of copying it. std::move turns an lvalue into an rvalue:
cat > move.cpp <<'EOF'
#include <iostream>
#include <string>
#include <utility>
int main() {
std::string a = "data besar yang panjang sekali";
std::string b = std::move(a);
std::cout << "b: " << b << "\n";
std::cout << "a (kosong setelah move): " << a.size() << "\n";
}
EOF
g++ -std=c++20 move.cpp -o move
./movestd::string b = std::move(a) moves a's internal buffer to b — an O(1) operation instead of copying the whole string. After the move, a is in a valid but unspecified state, usually empty. Move constructors and move assignment are the reason std::vector can grow without paying copy costs.
A class that manages resources can define a move constructor that steals resources instead of copying them. The compiler generates it automatically when all members are movable. This is one reason modern code is faster than C++98 code that copied everything.
std::optional<T> states a value that may not exist, std::variant<T1, T2> stores one value out of several possible types, and std::any stores a value of any type at the cost of dynamic allocation. Episode 10 covers optional and variant in depth; here you see any:
cat > any.cpp <<'EOF'
#include <iostream>
#include <any>
int main() {
std::any nilai = 42;
std::cout << std::any_cast<int>(nilai) << "\n";
nilai = std::string("teks");
if (nilai.type() == typeid(std::string)) {
std::cout << std::any_cast<std::string>(nilai) << "\n";
}
}
EOF
g++ -std=c++20 any.cpp -o any
./anystd::any_cast<int>(nilai) retrieves the value from any if the type matches, and throws std::bad_any_cast if not. Use any sparingly — a type unknown at compile time is usually a sign of a design that could be simplified with variant.
constexpr makes functions and objects computed at compile time when possible. Since C++20, constexpr supports loops, std::vector, and std::string, so many computations can move from runtime to compile time:
cat > constexpr20.cpp <<'EOF'
#include <iostream>
#include <array>
constexpr int jumlah(const std::array<int, 3>& a) {
int total = 0;
for (int x : a) {
total += x;
}
return total;
}
int main() {
constexpr std::array<int, 3> data{10, 20, 30};
constexpr int hasil = jumlah(data);
std::cout << hasil << "\n";
}
EOF
g++ -std=c++20 constexpr20.cpp -o constexpr20
./constexpr20constexpr int hasil = jumlah(data) runs jumlah at compile time — the result is embedded in the binary with zero runtime cost. Concepts (C++20) give names to type requirements, replacing the hard-to-read SFINAE, and were already covered in episode 10.
Coroutines (C++20) let a function suspend and resume without blocking a thread. co_await waits for an async operation, co_return returns a result. Coroutines are the foundation of asynchronous I/O and lightweight generators:
#include <coroutine>
// co_await async_call(); // suspend without blocking the thread
// co_return hasil; // finish the coroutineCoroutine programming requires the right task and awaiter types — complex enough for its own dedicated discussion. What matters to understand: coroutines enable thousands of concurrent async operations without thousands of threads, the pattern used by modern servers and game engines.
Modules (C++20) replace #include with self-contained compilation units. A module exports symbols explicitly with export, and is compiled only once instead of being reprocessed in every file that includes it:
cat > math.cppm <<'EOF'
export module math;
export int kali(int a, int b) {
return a * b;
}
EOF
g++ -std=c++20 -fmodules-ts math.cppm -o math.oexport module math; defines the module, and export int kali(...) exposes the function to module users. Build with -fmodules-ts in GCC. Modules make code cleaner than headers and speed up compilation of large projects.
Info
Module support is still evolving in compilers. For production projects, make sure your toolchain supports the features you use before fully switching away from headers.
Here's what to take away:
auto, range-based loops, and structured bindings make code concise and safe.std::move.optional, variant, and any express possible values explicitly.constexpr moves computations to compile time.In the next episode, episode 17, we'll discuss architecture and design patterns — the factory, singleton, and observer patterns, dependency injection, adapter, and decorator, domain-driven design and layered architecture, as well as modularization for organizing large-scale code.