You may understand loops, classes, and raw pointers, yet code using auto, lambdas, smart pointers, structured bindings, and ranges can look like another language. Those five tools arrived across C++11, C++17 and C++20, and a single inventory program uses all of them together: it sorts three products by price, drops the item with zero stock, and prints a stock value of 9800. Compiling that program and predicting its output turns the version names into something you can actually see. For the wider order, from a first program through to the STL, follow C++ Tutorial: Complete Learning Path in 12 Weeks.
Modern C++ features: what the label actually covers
"Modern C++" means the safer, more expressive style introduced across successive C++ standards. It is not one library or one isolated feature.
C++11 introduced
auto,nullptr, range-basedfor, lambdas, move semantics, and smart pointers.C++14 added generic lambdas, so
[](const auto& item) { return item.stock > 0; }accepts any type that has astockmember.C++17 added structured bindings and
std::optional.C++20 added ranges and concepts.
These additions complement, rather than replace, the classic classes and inheritance covered in OOP for Teaching CS Exams: Classes and Inheritance. A class still models the data; auto, lambdas and smart pointers change how you name types, pass behaviour, and say which object owns a resource.
Save the inventory program as modern.cpp, then compile and run it with:
c++ -std=c++20 modern.cpp && ./a.outAn older compiler or a command without the C++20 option may reject the ranges syntax.
Start with type deduction, safer nulls, and range-based loops
Consider this warm-up:
std::vector<int> scores{42, 67, 91};
auto total = 0;
for (const auto score : scores) total += score;The compiler infers total as an int. Each score is also an int, and const makes it read-only inside that iteration. The calculation is 42 + 67 + 91 = 200, so the total printed would be 200. auto asks the compiler to infer a static type. It does not make C++ dynamically typed.
References change the meaning. auto value = scores[0]; copies 42, while auto& value = scores[0]; makes value an alias for the first vector element. Changing the alias changes the element.
For pointers, use nullptr as the typed null-pointer value. In new code, avoid 0 or NULL when you mean a pointer. If loops, arrays, and pointer foundations need revision, first read C Programming for Teaching CS Exams: Key Concepts.
Build one runnable modern C++ inventory report
The program below sorts products by price, filters out items with no stock, and totals the remaining inventory value.
#include <algorithm>
#include <iostream>
#include <memory>
#include <ranges>
#include <string>
#include <utility>
#include <vector>
struct Product {
std::string name;
int price;
int stock;
};
struct Report {
int stock_value = 0;
};
int main() {
std::vector<Product> products{
{"Laptop Stand", 2200, 2},
{"Mouse", 900, 0},
{"Keyboard", 1800, 3}
};
std::ranges::sort(products, {}, &Product::price);
auto in_stock = products | std::views::filter(
[](const Product& product) { return product.stock > 0; }
);
auto report = std::make_unique<Report>();
for (const auto& [name, price, stock] : in_stock) {
std::cout << name << ": " << price << " x " << stock << '\n';
report->stock_value += price * stock;
}
std::cout << "Stock value: " << report->stock_value << '\n';
}Compiled and run with the command above, it prints:
Keyboard: 1800 x 3
Laptop Stand: 2200 x 2
Stock value: 9800The program combines an owning std::vector, a projection-based ranges sort, a lambda predicate, a lazy filter view, structured bindings, and exclusive ownership through std::unique_ptr. Each tool solves a specific problem. None is a universal replacement for ordinary loops or stack objects.
Trace the lambda, range pipeline, and structured bindings
std::ranges::sort orders products by the projected Product::price. The order becomes Mouse at 900, Keyboard at 1800, then Laptop Stand at 2200. The lambda returns true only when stock > 0, so Mouse with stock 0 disappears. The view therefore yields Keyboard followed by Laptop Stand.
Now trace the total:
Keyboard contributes
1800 x 3 = 5400.Laptop Stand contributes
2200 x 2 = 4400.The report stores
5400 + 4400 = 9800.
In const auto& [name, price, stock], the structured binding gives readable names to the three aggregate members. The reference avoids copying each Product, and const prevents mutation through those names.
The in_stock view is lazy. It describes how to traverse products; it does not build a second vector. Keep products alive, and do not structurally change it, while the view is in use.

Understand smart-pointer ownership and moving a value
auto report = std::make_unique<Report>(); creates one clear owner. When that owner goes out of scope, cleanup is automatic, so there is no manual delete. A stack variable would also be valid for this small Report; the smart pointer is here to teach ownership, not because the object must use heap storage.
Now extend the example:
auto archived = std::move(report);Ownership moves to archived. Afterwards, report == nullptr is true, while archived->stock_value is still 9800. Writing auto archived = report; instead is a compile-time error because std::unique_ptr cannot be copied.
Use std::shared_ptr only when ownership is genuinely shared. It should not be the default escape from deciding which object owns a resource.

Common modern C++ mistakes and how to repair them
Confusing an auto copy with a reference
Start with Product keyboard{"Keyboard", 1800, 3};. After auto copy = keyboard; copy.price = 999;, keyboard.price remains 1800 because copy is separate. After auto& alias = keyboard; alias.price = 999;, the original price becomes 999. Name whether you intend to copy or alias instead of choosing syntax only because it is short.
Capturing a lambda by value or reference
Set int limit = 5;, create [limit] { return limit; } and [&limit] { return limit; }, then set limit = 9. The value-capturing lambda returns 5; the reference-capturing lambda returns 9. Never return a reference-capturing lambda if it can outlive the local variable it references.
Losing track of lifetime and ownership
Do not return a view over a local vector. Return an owning container instead. Do not dereference a moved-from smart pointer; test it or assign it a new resource first. Do not mix new or delete into this program; prefer automatic storage or a standard smart pointer.
How tests and interviews turn these features into questions
Common assessment tasks ask you to predict output, decide whether code copies or aliases, spot a compile-time ownership error, or trace a lambda and range pipeline. In an interview, explain both the result and the reason. Under exam timing the same habit decides what you attempt first, as Coding Round Strategy: Triage, Time-Box, Bank Marks sets out.
Try these before reading the answers:
Change Mouse stock from
0to4. What is the order and total?Change the filter to
stock >= 3. Which product remains?Move
reportintoarchived. What can each pointer access?
Answers
Sorted output is Mouse, Keyboard, Laptop Stand. The total is
900 x 4 + 1800 x 3 + 2200 x 2 = 3600 + 5400 + 4400 = 13400.Only Keyboard remains, and its total is
1800 x 3 = 5400.report == nullptristrue, andarchived->stock_valueis9800.
One final self-check: why use const auto& [name, price, stock] instead of auto [name, price, stock] when the loop only reads? The reference avoids copying the aggregate, and const prevents mutation through the bindings.
The short version and the next runnable step
Use auto to infer types without hiding intent. Prefer clear ownership, use lambdas for local behaviour, treat views as lazy non-owning traversal, and trace references and moves carefully.
For structured study with MCQs and coding questions, the Coding & DSA Courses for Placements hub lists the language courses, C++ included. Then compile the program above, apply each of the three exercise changes to the original code separately, and predict the output before you run it.




