Strings, streams and formatting

Use string and string_view without dangling views, choose between iostream and std::format, and parse text and paths robustly.

std::string and std::string_view

std::string owns its bytes and manages capacity. std::string_view is a non-owning pointer plus length: cheap to pass, and dangerous the moment the underlying storage dies.

#include <string>
#include <string_view>
#include <iostream>

// takes any contiguous char range without allocating
void log_line(std::string_view sv) {
    std::cout << sv.size() << " bytes\n";
}

std::string_view bad() {
    std::string local = "temporary";
    return local;                 // DANGLING: local is destroyed on return
}

std::string good() { return std::string("temporary"); }

int main() {
    std::string s = "hello world";
    std::string_view v = s;                  // fine: s outlives v
    log_line(s);
    log_line("a literal");                   // fine: string literal has static lifetime

    // view does not own: reassigning the string can invalidate the view
    s = "a much longer replacement string that forces reallocation";
    std::cout << v.size() << "\n";          // v now points into freed memory

    std::string_view sub = s.substr(0, 5);   // substr returns a view into s
    std::cout << sub << "\n";
}
  • Never return a string_view built from a local string, and never store one as a member pointing at a temporary.
  • string_view is not NUL-terminated. Passing v.data() to a C API reads past the end unless the view spans the whole string.
  • reserve before a loop that appends, and prefer append with a length over repeated + which allocates each time.
  • std::string comparison is lexicographic and value-based; comparing it to a const char * with == does the right thing, unlike comparing two raw pointers.

iostream, std::format and std::print

#include <format>
#include <print>          // C++23
#include <iostream>
#include <string>

int main() {
    int id = 7;
    double ratio = 0.123456;

    // C++20: build a string, locale-independent, type-safe
    std::string msg = std::format("id={:04d} ratio={:.2f} name={}", id, ratio, "ada");
    std::cout << msg << '\n';

    // C++23: print directly to stdout, newline included
    std::print("id={:04d} ratio={:.2f}\n", id, ratio);
    std::print(stderr, "error code {}\n", 42);

    // iostream: still the tool for custom types and stream state
    std::cerr << "id=" << id << '\n';
    std::cout << std::hex << 255 << '\n';        // sticky manipulator
    std::cout << std::dec;                        // remember to reset it
}
NeedReach forWhy
A formatted stringstd::formatType-safe, positional, locale-independent by default
Print to standard outputstd::print / printlnFaster than iostream, no manipulator state
A custom typestd::formatter specialisation or operator<<Both integrate with the modern and legacy paths
Unformatted binary outputstd::ostream::write / std::spanAvoids locale and formatting overhead
Reading structured inputParse with string_view and from_charsoperator>> is locale-sensitive and hard to validate
⚠️
std::format and std::print must be implemented by your standard library. If a build fails on the header rather than the call, check __cpp_lib_format and fall back to fmt or iostream. MSVC and recent libstdc++ and libc++ all provide it, but not every distribution is current.

Numbers, paths and files

#include <charconv>
#include <filesystem>
#include <fstream>
#include <optional>
#include <string_view>

std::optional<int> to_int(std::string_view sv) {
    int value{};
    auto [ptr, ec] = std::from_chars(sv.data(), sv.data() + sv.size(), value);
    if (ec != std::errc{} || ptr != sv.data() + sv.size()) return std::nullopt;
    return value;                       // from_chars is locale-independent and fast
}

void read_config(const std::filesystem::path& p) {
    std::ifstream in(p);
    if (!in) throw std::runtime_error("cannot open " + p.string());

    std::string line;
    while (std::getline(in, line)) {     // strips the newline
        std::string_view sv{line};
        if (sv.starts_with('#')) continue;
    }
    // in closes here; do not rely on the destructor for anything you must verify
    if (in.bad()) throw std::runtime_error("read error");
}

int main() {
    namespace fs = std::filesystem;
    fs::path p = fs::path("data") / "input.csv";
    std::cout << p.extension().string() << " " << fs::absolute(p).string() << "\n";
    std::cout << fs::exists(p) << "\n";   // throws on permission errors unless you pass an error_code
}

FAQ

Is std::string always heap allocated?
Most implementations use a small-string optimisation, storing up to about 15 characters inline. Anything longer allocates. That makes short strings cheap and turns string_view into a genuine win for read-only parameters.
Why is std::endl slow?
std::endl flushes the stream, forcing a write syscall every time. Use '\n' for a newline and flush explicitly only when you need the output visible, such as before a crash or when prompting a user.

Modern C++ basics References, const correctness and value categories

Last refreshed 2026-09-18.