Chapter 23 of 23

Modern C++ — C++11 to C++20 Features

A curated tour of the most impactful modern C++ features from C++11 through C++20, with a career roadmap for competitive programming and SDE roles at Indian and global companies.

Meritshot14 min read
C++C++11C++14C++17C++20Modern C++autoconstexprConceptsstd::optionalstd::variant
All C++ Chapters

Modern C++ — C++11 to C++20 Features

C++ did not merely evolve after 2011 — it was transformed. The standard that emerged as C++11 was so different from C++03 that Bjarne Stroustrup called it "a new language." Each subsequent standard (C++14, C++17, C++20) added features that make code safer, faster to write, and easier to reason about.

If you are preparing for competitive programming on Codeforces or LeetCode, or targeting SDE roles at companies like Google, Microsoft, or Infosys, knowing modern C++ gives you a measurable edge. This chapter is a curated tour of the features that matter most, with real examples and a roadmap at the end.


auto and Type Deduction

auto tells the compiler to deduce the type of a variable from its initialiser. It eliminates verbose type names without losing type safety:

#include <iostream>
#include <vector>
#include <map>

int main() {
    auto x = 42;                           // int
    auto pi = 3.14159;                     // double
    auto name = std::string("Meritshot"); // std::string

    std::vector<std::pair<int, std::string>> students = {{1, "Ananya"}, {2, "Rohan"}};

    // Without auto: std::vector<std::pair<int,std::string>>::iterator it = ...
    // With auto: clean and clear
    for (auto it = students.begin(); it != students.end(); ++it) {
        std::cout << it->first << ": " << it->second << "\n";
    }
}

auto is especially valuable with complex STL types. It never changes the actual type — it just lets the compiler fill it in.

decltype is the companion feature: it deduces the type of an expression without evaluating it.

int a = 5;
double b = 3.0;
decltype(a + b) result = a + b; // result is double

Range-Based For

C++11's range-based for loop works with any type that has begin() and end() methods — all STL containers, arrays, and anything you make iterable:

#include <iostream>
#include <vector>

int main() {
    std::vector<int> scores = {85, 92, 78, 95, 60};

    // By value (copies each element)
    for (int s : scores) {
        std::cout << s << " ";
    }

    // By const reference (no copy, read-only) — prefer this for objects
    for (const auto& s : scores) {
        std::cout << s << " ";
    }

    // By reference (can modify in place)
    for (auto& s : scores) {
        s += 5; // add 5 to each score
    }
}

Always use const auto& for containers of objects (strings, structs) to avoid copying.


nullptr

C++11 replaced the old NULL macro (which was typically defined as 0, an integer) with the keyword nullptr, which has its own type (std::nullptr_t):

void process(int* ptr) {
    if (ptr != nullptr) {
        std::cout << *ptr << "\n";
    }
}

int main() {
    process(nullptr); // clear intent: passing a null pointer
    // process(0);    // compiles but misleading — was it an int?
    // process(NULL); // could match overloads taking int — ambiguous
}

Always use nullptr instead of NULL or 0 for pointer values.


Scoped Enumerations — enum class

Classic enum pollutes the enclosing namespace and its values implicitly convert to int. enum class fixes both problems:

#include <iostream>

// Classic enum — pollutes scope, implicit int conversion
enum Status { ACTIVE, INACTIVE, PENDING }; // ACTIVE is in global scope

// Scoped enum — names are inside the enum, no implicit int conversion
enum class AccountStatus { Active, Inactive, Pending };
enum class HttpStatus  { OK = 200, NotFound = 404, Error = 500 };

int main() {
    AccountStatus s = AccountStatus::Active;

    if (s == AccountStatus::Active) {
        std::cout << "Account is active\n";
    }

    HttpStatus code = HttpStatus::NotFound;
    // int n = code; // Error: no implicit conversion — safer!
    int n = static_cast<int>(code); // explicit cast required
    std::cout << "HTTP code: " << n << "\n"; // 404
}
Featureenumenum class
Name scopingPollutes enclosing scopeScoped to enum name
Implicit int conversionYesNo
Forward declarableOnly with explicit typeYes
SafetyLowHigh

Prefer enum class in all new code.


constexpr — Compile-Time Computation

constexpr marks a variable or function as potentially evaluable at compile time. This enables compile-time constants, avoiding runtime overhead:

#include <iostream>

constexpr int factorial(int n) {
    return n <= 1 ? 1 : n * factorial(n - 1);
}

int main() {
    constexpr int f10 = factorial(10); // computed at compile time
    std::cout << "10! = " << f10 << "\n"; // 3628800

    // Array size known at compile time — no VLA needed
    constexpr int SIZE = 100;
    int arr[SIZE]; // perfectly valid
}

In C++14 and later, constexpr functions can contain loops and local variables. In C++17 and C++20 the rules were further relaxed to allow if, switch, and even dynamic allocation.


Structured Bindings (C++17)

Structured bindings let you unpack tuples, pairs, structs, and arrays into named variables in one line:

#include <iostream>
#include <map>
#include <tuple>

std::tuple<std::string, int, double> getEmployee() {
    return {"Priya Sharma", 5, 1800000.0}; // name, experience, salary (INR)
}

int main() {
    // Unpack a tuple
    auto [name, exp, salary] = getEmployee();
    std::cout << name << " | " << exp << " yrs | INR " << salary << "\n";

    // Unpack a map entry
    std::map<std::string, int> scores = {{"Rahul", 95}, {"Sneha", 88}};
    for (const auto& [student, score] : scores) {
        std::cout << student << ": " << score << "\n";
    }

    // Unpack a pair
    auto [min, max] = std::make_pair(10, 90);
    std::cout << min << " to " << max << "\n";
}

Structured bindings dramatically reduce the need for .first / .second and numbered std::get<N>() calls.


std::optional (C++17)

std::optional<T> represents a value that may or may not be present — a type-safe alternative to returning -1, nullptr, or a sentinel value:

#include <iostream>
#include <optional>
#include <string>
#include <vector>

std::optional<int> findScore(const std::vector<std::pair<std::string,int>>& db,
                              const std::string& name) {
    for (const auto& [n, score] : db) {
        if (n == name) return score;
    }
    return std::nullopt; // no value
}

int main() {
    std::vector<std::pair<std::string,int>> scores = {
        {"Arjun", 92}, {"Divya", 87}, {"Karan", 95}
    };

    auto result = findScore(scores, "Divya");
    if (result.has_value()) {
        std::cout << "Score: " << result.value() << "\n"; // 87
    }

    auto missing = findScore(scores, "Rahul");
    std::cout << "Rahul: " << missing.value_or(0) << "\n"; // 0

    // Dereference directly (UB if nullopt — always check first)
    if (result) std::cout << "Found: " << *result << "\n";
}

std::optional is ideal for functions that search, parse, or look up values that might not exist.


std::variant (C++17)

std::variant<T1, T2, ...> holds exactly one value of one of its listed types — a type-safe union. It replaces C-style union and eliminates the need for manual type tags:

#include <iostream>
#include <variant>
#include <string>

using Result = std::variant<int, std::string, double>;

Result process(int input) {
    if (input < 0)  return std::string("Error: negative input");
    if (input == 0) return 0.0;          // double
    return input * 2;                    // int
}

int main() {
    for (int val : {-1, 0, 5}) {
        Result r = process(val);

        // std::visit applies a callable to the active type
        std::visit([](auto&& v) {
            std::cout << "Result: " << v << "\n";
        }, r);
    }
    // Result: Error: negative input
    // Result: 0
    // Result: 10
}

std::visit with a generic lambda (or an overloaded visitor) dispatches to the correct handler at runtime based on which type is active.


Concepts (C++20)

Templates are powerful but produce notoriously unreadable error messages when misused. Concepts constrain template parameters with readable, enforceable requirements:

#include <iostream>
#include <concepts>

// Define a concept: T must support operator+ and be constructible from 0
template<typename T>
concept Addable = requires(T a, T b) {
    { a + b } -> std::convertible_to<T>;
};

// Constrain the template
template<Addable T>
T sum(T a, T b) {
    return a + b;
}

int main() {
    std::cout << sum(3, 4)       << "\n"; // 7     (int)
    std::cout << sum(1.5, 2.5)   << "\n"; // 4.0   (double)
    // sum("hello", "world");             // Clear compile-time error
}

The standard library provides many ready-made concepts in <concepts>: std::integral, std::floating_point, std::same_as, std::convertible_to, std::invocable, etc.


Modules Overview (C++20)

Traditional #include is slow: every translation unit parses the same headers repeatedly. Modules replace header files with a compiled binary interface that is parsed once:

// math_utils.cppm  (module interface file)
export module math_utils;

export int square(int x) { return x * x; }
export double pi = 3.14159;
// main.cpp
import math_utils;
import <iostream>;

int main() {
    std::cout << square(7) << "\n"; // 49
}

Modules offer faster compilation, no macro leakage across translation units, and cleaner dependency graphs. Compiler support (GCC 11+, Clang 16+, MSVC 2019+) is still maturing as of 2026, but major codebases are beginning to adopt them.


std::span (C++20)

std::span<T> is a non-owning, lightweight view over a contiguous range of T. It replaces the common pattern of passing a raw pointer and a length:

#include <iostream>
#include <span>
#include <vector>
#include <numeric>

// Works with arrays, vectors, or any contiguous range
void printAll(std::span<const int> data) {
    for (int v : data) std::cout << v << " ";
    std::cout << "\n";
}

int sumSpan(std::span<const int> data) {
    return std::reduce(data.begin(), data.end(), 0);
}

int main() {
    int arr[] = {1, 2, 3, 4, 5};
    std::vector<int> vec = {10, 20, 30, 40};

    printAll(arr);          // 1 2 3 4 5
    printAll(vec);          // 10 20 30 40
    printAll({arr + 1, 3}); // 2 3 4  — subspan

    std::cout << sumSpan(vec) << "\n"; // 100
}

std::span carries a pointer and a length. It involves no ownership, no allocation, and no copying.


Feature Comparison Table

FeatureStandardKey Benefit
auto / decltypeC++11Eliminate verbose type names
Range-based forC++11Cleaner iteration over containers
nullptrC++11Type-safe null pointer constant
Move semantics / &&C++11Zero-cost resource transfer
Lambda expressionsC++11Inline anonymous functions
enum classC++11Scoped, type-safe enumerations
constexprC++11/14/17Compile-time computation
Generic lambdasC++14auto parameters in lambdas
Structured bindingsC++17Unpack tuples/pairs/structs cleanly
std::optionalC++17Type-safe "maybe" values
std::variantC++17Type-safe tagged unions
if constexprC++17Compile-time branching in templates
ConceptsC++20Constrained, readable templates
ModulesC++20Fast, clean dependency model
std::spanC++20Non-owning contiguous view
Ranges (std::views)C++20Composable, lazy range operations
CoroutinesC++20Async / generator patterns

What to Learn Next — Career Roadmap

For Competitive Programming (Codeforces / LeetCode / CodeChef)

  • Master STL deeply: std::vector, std::set, std::map, std::priority_queue, std::deque.
  • Learn std::sort with custom comparators and lambda predicates.
  • Practice auto, range-based for, and structured bindings for clean, fast code.
  • Understand std::pair and std::tuple for multi-key sorting.
  • Study graph algorithms (BFS, DFS, Dijkstra) implemented in C++ — they appear in every competitive programming round.
  • Target: solve 200+ problems on Codeforces and reach 1200+ rating before campus placements.

For SDE Roles at Indian Companies (TCS, Infosys, Wipro, HCL)

  • Get comfortable with C++11 features (the minimum expected at service companies).
  • Learn object-oriented design: SOLID principles, design patterns (Factory, Singleton, Observer).
  • Practice data structures and algorithms — most TCS/Infosys online tests are algorithmic.
  • Build 1–2 projects using C++ with file I/O, basic networking (sockets), or embedded-style code.

For SDE Roles at Product Companies (Google India, Microsoft, Flipkart, PhonePe, Meesho, CRED)

  • Move semantics and smart pointers (std::unique_ptr, std::shared_ptr) are interview essentials.
  • Concurrency: std::thread, std::mutex, std::atomic — senior SDE interviews almost always include threading questions.
  • Templates and metaprogramming basics: at least understand how STL containers are templated.
  • Modern C++ (C++17 features) is expected at Google and Flipkart engineering roles.
  • System design: understand how C++ is used in databases (RocksDB), game engines (Unreal), and high-frequency trading systems.
  • Expected salary range for C++ SDE-2 at Google India, Amazon India, or Flipkart: INR 40–80 LPA (2025–2026).

For Systems and Embedded Roles

  • Learn constexpr, std::array, and avoid dynamic allocation — critical for embedded systems.
  • Study RAII, custom allocators, and placement new.
  • Understand the C++ memory model and std::atomic memory orders.
  • Study MISRA C++ or automotive-grade coding standards if targeting automotive (Bosch, Aptiv) or aerospace.

Common Pitfalls

1. Using auto where type clarity matters auto x = getEmployee(); is fine if the return type is obvious. But auto x = compute(); in a large codebase can make code harder to understand. Use auto where the type is either obvious or unimportant (iterators, lambda types); name the type explicitly when it communicates intent.

2. std::optional with expensive types optional<T> stores T by value inside the optional. For large objects, consider optional<std::unique_ptr<T>> or restructure your design.

3. std::variant index vs type access Accessing the wrong type with std::get<T> throws std::bad_variant_access. Always check with std::holds_alternative<T>(v) before accessing, or use std::visit.

4. Misusing constexpr A function marked constexpr can still be called at runtime (if the arguments are not compile-time constants). It is a permission, not a guarantee of compile-time evaluation. Use consteval (C++20) if you want a guarantee.

5. Structured bindings and references auto [a, b] = pair; copies. auto& [a, b] = pair; gives references. For read-only access to expensive types, use const auto& [a, b] = pair;.


Practice Exercises

  1. Rewrite a function that returns {-1, ""} on failure to use std::optional<std::pair<int, std::string>> instead. Show both the function and the call site.

  2. Create a std::variant<int, double, std::string> and write a std::visit overload set (using a struct with three operator() overloads) that prints the value with its type name.

  3. Write a constexpr function isPrime(int n) and use it to initialise a constexpr array of the first 10 prime numbers.

  4. Define a concept Sortable that requires a type to support operator<. Write a function template constrained by Sortable that finds the minimum element in a std::vector<T>.

  5. Use std::span to write a function rotate90(std::span<std::span<int>> matrix) (conceptually — describe the approach) and explain why span is better than passing a raw 2D array pointer with separate row/col parameters.


Summary

  • auto eliminates verbose type declarations without sacrificing type safety; use it freely with iterators and complex types.
  • Range-based for loops work with all STL containers; prefer const auto& for read-only access to avoid copies.
  • nullptr is type-safe; always use it instead of NULL or 0 for pointer values.
  • enum class scopes enumerator names and removes implicit integer conversions — use it in all new code.
  • constexpr enables compile-time computation; consteval (C++20) enforces it.
  • Structured bindings (C++17) cleanly unpack pairs, tuples, and structs into named variables.
  • std::optional is the correct way to represent a value that may be absent; eliminates sentinel return values.
  • std::variant is a type-safe union; use std::visit with a callable to handle all active types.
  • Concepts (C++20) constrain templates with readable requirements and produce clear error messages.
  • Modules (C++20) replace headers for faster compilation and cleaner dependency management.
  • std::span is a lightweight, non-owning view over contiguous data — prefer it over raw pointer + length pairs.
  • For career growth: master C++11/14 for service companies, add C++17 for product companies, and add concurrency, move semantics, and concepts for senior/staff roles at Google India, Microsoft, Flipkart, and global firms.