Skillversitas

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

  • auto is 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 of expr without 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

  • nullptr has the type std::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, and get<>().

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 the bool operator checks for the presence of a value.
  • value() or operator* 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 — throws bad_variant_access if 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_if in 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++.