Practical Standard Library and C Interoperability
Error Handling, Tooling, and Multi-File Programs

8.10 Practical Standard Library and C Interoperability

The standard library is more than containers and algorithms. Real programs read configuration, measure time, draw randomness, store callbacks, and call C libraries. Each boundary needs the same discipline as heap ownership: state who owns what, how errors propagate, and when resources close.

Files and paths

std::filesystem::path represents paths without hand-rolled separator logic. Prefer the std::error_code overloads when a missing file or permission failure is an ordinary outcome, not every filesystem failure should throw.

#include <filesystem>
#include <system_error>

int main() {
    std::error_code error;
    const std::filesystem::path path{"config.json"};
    const bool exists = std::filesystem::exists(path, error);
    return !error && exists ? 0 : 1;
}
Check a path without throwing for an expected missing file.

For reading text, combine streams or std::ifstream with RAII so files close on every exit path. Lesson 7.1 showed custom deleters; the same idea applies to FILE* from the C library.

Time with chrono

Pass std::chrono::milliseconds or std::chrono::steady_clock::time_point through APIs instead of bare integers whose unit is ambiguous. steady_clock is appropriate for measuring elapsed time; system_clock reflects wall time and can jump when the OS adjusts the clock.

#include <chrono>
#include <thread>

void wait(std::chrono::milliseconds delay) {
    std::this_thread::sleep_for(delay);
}

int main() {
    wait(std::chrono::milliseconds{10});
    return 0;
}
Express a timeout with a typed duration.

Randomness: engine + distribution

Separate the engine (state) from the distribution (mapping). Seed deliberately for reproducible tests; use a platform cryptographic API when security depends on unpredictability, std::random_device is not a portable crypto guarantee.

#include <random>

int main() {
    std::mt19937 engine{12345};
    std::uniform_int_distribution<int> die{1, 6};
    const int roll = die(engine);
    return roll >= 1 && roll <= 6 ? 0 : 1;
}
Reproducible test dice roll.

std::function vs templates for callbacks

A lambda has a unique compiler-generated type. Templates preserve that type at compile time (no allocation, direct call). std::function type-erases callables behind a small buffer or heap allocation, use it at API boundaries that must store callbacks homogeneously, not in every hot inner loop.

C interoperability and RAII

C APIs expose raw pointers, handles, and error codes. Confine them behind a thin C++ adapter: wrap acquisition and release in RAII, translate errors into std::expected or exceptions at the C++ boundary, and use extern "C" only where C linkage is required. Never let C++ exceptions escape into C callers.

#include <cstdio>
#include <memory>
#include <string>

struct FileCloser {
    void operator()(std::FILE* f) const noexcept {
        if (f) std::fclose(f);
    }
};

using UniqueFile = std::unique_ptr<std::FILE, FileCloser>;

UniqueFile openForRead(const std::string& path) {
    return UniqueFile{std::fopen(path.c_str(), "rb")};
}
RAII wrapper for FILE* with explicit open failure.

Capstone 10.4 combines these ideas with binary parsing (std::span<const std::byte>) and bounded concurrency.

Practice: extend openForRead to return std::optional<UniqueFile> or std::expected<UniqueFile, int> using errno. Write a test that opens a missing path and asserts the error path without leaking handles.

Chapter 8 Knowledge CheckQuestion 1 of 9

Where should an ordinary non-inline function definition usually live in a multi-file C++ program?

Finished reading this lesson?