Skip to content

Modern Utilities (C++17)

C++17 introduced several "vocabulary types" designed to replace unsafe C-style constructs (like raw null pointers, magic constants, or unsafe unions) with modern, type-safe abstractions.


1. std::optional (Safe Presence/Absence representation)

A function might fail to return a value (e.g. database query, config parsing). Previously, developers used magic values (like -1, 0, or returning a nullptr pointer). std::optional explicitly declares that a value may or may not exist, forcing safe checks.

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

// Dummy helper
bool is_valid_number(const std::string& s) {
    return !s.empty() && s.find_first_not_of("0123456789") == std::string::npos;
}

// Returns an int if parsing is successful, otherwise returns std::nullopt
std::optional<int> parse_sensor_id(const std::string& input) {
    if (!is_valid_number(input)) {
        return std::nullopt; // Represents absence of value
    }
    try {
        return std::stoi(input); // Return actual int
    } catch (...) {
        return std::nullopt;
    }
}

int main() {
    auto id = parse_sensor_id("105");

    // 1. Explicit Check
    if (id.has_value()) { // Or simply: if (id)
        std::cout << "Sensor ID: " << id.value() << "\n"; // Or: *id
    }

    // 2. Default Fallback
    // If id is empty, fallback to 0
    int active_id = id.value_or(0); 

    return 0;
}

2. std::variant (Type-Safe Unions)

A standard C union can hold different types in the same memory location, but it does not know which type is currently active, and cannot safely store complex types like std::string.

std::variant is a type-safe union. It knows which type it contains, runs destructors when types change, and prevents invalid access.

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

// Define a variant that can hold an int, a double, or a string
using Payload = std::variant<int, double, std::string>;

int main() {
    Payload data;

    data = 42;             // Currently holds int
    data = "Pi in sky";    // Transferred to std::string

    // 1. Safe Type Queries
    if (std::holds_alternative<std::string>(data)) {
        std::cout << "String: " << std::get<std::string>(data) << "\n";
    }

    // 2. Safe Pointer Queries
    if (auto pVal = std::get_if<int>(&data)) {
        std::cout << "Int: " << *pVal << "\n"; // Safely returns nullptr if type mismatch
    }

    return 0;
}

Pattern Matching with std::visit

The most powerful way to process a std::variant is using a "Visitor" object with std::visit.

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

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

struct Printer {
    void operator()(int i) const { std::cout << "Integer: " << i << "\n"; }
    void operator()(double d) const { std::cout << "Double: " << d << "\n"; }
    void operator()(const std::string& s) const { std::cout << "String: " << s << "\n"; }
};

int main() {
    Payload2 payload = 3.14;

    // Calls the correct overload automatically
    std::visit(Printer{}, payload); 

    return 0;
}

3. std::any (Dynamic Type Erasure)

std::any can hold a value of absolutely any copyable type. Unlike std::variant, you do not need to pre-declare the list of allowed types. - Overhead: High. It often performs dynamic memory allocation (heap) to store values, and checking/retrieving values via std::any_cast requires runtime type validation.

#include <any>
#include <string>
#include <iostream>

int main() {
    std::any container = 100; // Stores int
    container = std::string("Flexible storage"); // Stores string

    try {
        std::string text = std::any_cast<std::string>(container);
        std::cout << text << "\n";
    } catch (const std::bad_any_cast& e) {
        std::cout << "Invalid cast: " << e.what() << "\n";
    }

    return 0;
}

4. Path and File System Operations (std::filesystem)

The std::filesystem library provides a robust, platform-independent API to inspect and manage file systems.

#include <filesystem>
#include <iostream>

namespace fs = std::filesystem;

int main() {
    fs::path log_dir = "/tmp/var/log/my_app";

    // 1. Directory Checks & Creation
    if (!fs::exists(log_dir)) {
        fs::create_directories(log_dir); // Creates recursive paths if needed
    }

    // 2. Inspecting Files & Metadata
    fs::path config_file = "config.txt";
    if (fs::exists(config_file)) {
        std::cout << "File size: " << fs::file_size(config_file) << " bytes\n";
    }

    // 3. Iterating Directories
    fs::path current_dir = fs::current_path();
    std::cout << "Files in " << current_dir << ":\n";
    for (const auto& entry : fs::directory_iterator(current_dir)) {
        std::cout << "- " << entry.path().filename().string() 
                  << " [" << (entry.is_directory() ? "DIR" : "FILE") << "]\n";
    }

    return 0;
}