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_viewbuilt from a localstring, and never store one as a member pointing at a temporary. string_viewis not NUL-terminated. Passingv.data()to a C API reads past the end unless the view spans the whole string.reservebefore a loop that appends, and preferappendwith a length over repeated+which allocates each time.std::stringcomparison is lexicographic and value-based; comparing it to aconst 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
}| Need | Reach for | Why |
|---|---|---|
| A formatted string | std::format | Type-safe, positional, locale-independent by default |
| Print to standard output | std::print / println | Faster than iostream, no manipulator state |
| A custom type | std::formatter specialisation or operator<< | Both integrate with the modern and legacy paths |
| Unformatted binary output | std::ostream::write / std::span | Avoids locale and formatting overhead |
| Reading structured input | Parse with string_view and from_chars | operator>> 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.Related
Modern C++ basics References, const correctness and value categories
Last refreshed 2026-09-18.