Standard Library[lib]

std::println in C++23: print without std::cout or printf

Abstract

Write std::println("port {}", port) (C++23, header <print>): it formats each argument into its {} placeholder, writes the text to stdout and adds the newline. A format string with a placeholder that has no argument, or a spec the type does not take, does not compile. std::print does the same without the newline. Before C++23, end std::cout lines with '\n': std::endl also flushes the stream, once for every line.

A row of seven small square cells, the first three hatched. A magenta arrow leads from the row to a tall rectangle on the right holding four horizontal lines of different lengths, like lines of text.
Figure 1 — Text waits in a buffer until something flushes it to the screen.

To print a line in C++23, call std::println("port {}", port); from <print>: it formats each argument into its {} placeholder, writes the text to standard output and adds the newline. The format string is checked at compile time, so a placeholder with no argument, or a spec the type does not take, does not build. std::print is the same without the newline, and both take a FILE* or a stream as their first argument to write elsewhere. Before C++23, write std::cout << "port " << port << '\n'; and keep std::endl for when you want the flush.

This answer is written for C++23, where P2093R14 added std::print and std::println; they take the format strings of C++20’s std::format. C++26 adds std::dynamic_format for a format string built at run time. It also adds std::println() with no arguments (P3142R0), which all four compilers here accept in C++23 mode.

The samples were compiled and run with MSVC 19.44 and Clang 19.1 on Microsoft’s STL (Windows x64), and with GCC 16.2 (libstdc++) and Clang 23.1 (libc++) on Compiler Explorer (Linux x86-64), their output read through a pipe, not a terminal. An output shown is what all four printed unless the text names one. First releases: Since When’s C++23 table.

What changed: a line is printed with std::cout and std::endl, or printf std::println.

since C++23

Include <print>, then pass a format string and a value for each {}:

[Example 1:
#include <print>
#include <string>

int main() {
    int port = 8080;
    std::string host = "db.local";
    double load = 0.4567;
    std::println("port {}", port);             // port 8080
    std::println("{}:{}", host, port);         // db.local:8080
    std::println("{1} on {0}", host, port);    // 8080 on db.local
    std::println("[{:>8}]", port);             // [    8080]
    std::println("[{:06}]", port);             // [008080]
    std::println("load {:.2f}", load);         // load 0.46
    std::println("{{}} is a literal {}", "pair of braces");
    std::print("host {}", host);               // no newline
    std::print(", port {}\n", port);           // so add one
}

— end example]

All four printed:

[Example 2:
port 8080
db.local:8080
8080 on db.local
[    8080]
[008080]
load 0.46
{} is a literal pair of braces
host db.local, port 8080

— end example]

  • {} takes the next argument; {0} and {1} pick one by position.
  • After a colon comes the spec: >8 right-aligns in eight characters, 06 pads with zeros to six, .2f keeps two decimals. Hex, signs and the rest are in std::format’s specs for numbers, in C++ int to string.
  • {{ and }} print one brace each.
  • std::println() with no arguments prints an empty line.

std::cout before C++23

Before <print>, a line goes to std::cout from <iostream>, one << per piece, ended with '\n'; errors go to std::cerr:

[Example 3:
#include <iostream>
#include <string>
// ...
std::string host = "db.local";
int port = 8080;

std::cout << "connecting to " << host << ':' << port << '\n';
std::cerr << "warning: no TLS\n";       // errors go to std::cerr

— end example]

It printed connecting to db.local:8080 to standard output and warning: no TLS to standard error, on all four.

std::hex, std::fixed and std::setprecision stay set on std::cout for every later line, std::setw applies to the next item only, and std::println carries nothing from one call to the next:

[Example 4:
std::cout << std::hex << port << '\n';
std::cout << port << '\n';              // still hex
std::cout << std::dec << std::setw(8) << port << '\n';
std::cout << port << '\n';              // setw applied once

std::cout << std::fixed << std::setprecision(2) << load << '\n';
std::cout << 1.0 / 3 << '\n';           // still fixed, 2 digits
std::println("{:x} {}", port, port);    // nothing carries over

— end example]

With load at 0.5, all four printed:

[Example 5:
1f90
1f90
    8080
8080
0.50
0.33
1f90 8080

— end example]

The same line with std::println, std::cout and printf

One job per row, std:: left out, with p a void* holding 0x1f90. The cout lines end in << '\n' and the printf formats in \n; every row was printed all three ways and the texts compared.

Job std::println std::cout << printf
A value "{}", port port "%d", port
Two values "port {} on {}", port, host "port " << port << " on " << host "port %d on %s", port, host.c_str()
Zero padding "{:06}", port setfill('0') << setw(6) << port "%06d", port
Hex "{:x}", port hex << port << dec "%x", static_cast<unsigned>(port)
Two decimals "{:.2f}", load fixed << setprecision(2) << load "%.2f", load
A bool as text "{}", verbose boolalpha << verbose "%s", verbose ? "true" : "false"
A std::string "{}", host host "%s", host.c_str()
A void* "{}", p: 0x1f90 on all four p: 0000000000001F90 on Windows, 0x1f90 on Linux "%p", p: as cout

Every other row printed the same text all three ways on all four compilers.

A plain {} on a double is not cout << x. For 1.0 / 3, {} gave 0.3333333333333333 where cout and %g gave 0.333333; for 1234567.0, 1234567 where they gave 1.23457e+06 (all four). When translated output must not change, write the precision, as in {:.2f}.

Where std::println writes: stderr, a file, a stream

With no stream argument, std::println writes to C’s stdout. A FILE* first sends the line there instead, and a std::ostream& first picks the overload declared in <ostream>:

[Example 6:
std::println(stderr, "warning: port {} in use", port);
// ...
std::FILE* log = std::fopen(path.string().c_str(), "w");
if (!log) return 1;
std::println(log, "{}:{}", host, port);
std::fclose(log);
// ...
std::ostringstream os;
std::println(os, "{}:{}", host, port);  // the <ostream> overload

— end example]

Read back, the file and the string stream each held db.local:8080 on all four.

Mixing std::println and std::cout

std::println writes to stdout, not through std::cout. With the default std::ios::sync_with_stdio(true), the standard keeps the two in order: a synchronized cout writes as fputc on stdout would ([ios.members.static]). After sync_with_stdio(false) the order is up to the library:

[Example 7:
std::ios::sync_with_stdio(false);   // before any output
std::println("1 println");
std::cout << "2 cout\n";
std::println("3 println");
std::cout << "4 cout\n";
std::println(std::cout, "5 println(std::cout)");
std::println("6 println");

— end example]

Through a pipe, Microsoft’s STL and libc++ still printed 1 to 6 in order. GCC with libstdc++ printed:

[Example 8:
2 cout
4 cout
5 println(std::cout)
1 println
3 println
6 println

— end example]

In a program that turns synchronization off, send std::println through the stream: std::println(std::cout, …) kept its place among the cout lines. The Core Guidelines (SL.io.10) say to turn it off “unless you use printf-family functions”; through a pipe on libstdc++, std::println to stdout behaved like a printf call, and its lines came out after every cout line.

A wrong format string does not compile

std::println checks its format string against the argument types while compiling (P2216R3, applied to C++20 as a defect report). Two placeholders and one argument:

[Example 9:
std::string host = "db.local";
std::println("{}:{}", host);   // two placeholders, one argument

— end example]

All four rejected it, and std::println("host {:d}", host) too, where d asks for an integer. The error starts with call to consteval function 'std::basic_format_string<…>' is not a constant expression (MSVC: error C7595: … call to immediate function is not a constant expression). The note that says why differs by library:

Library Missing argument {:d} on a string
Microsoft’s STL _You_see_this_error_because_arg_id_is_out_of_range _Throw_format_error (MSVC), Invalid presentation type for string (Clang)
libstdc++ __invalid_arg_id_in_format_string __failed_to_parse_format_spec
libc++ Argument index outside the valid range The type option contains an invalid value for a string formatting argument

An extra argument is not an error: std::println("port {}", port, host) printed port 8080 on all four. In printf, a conversion that does not match its argument, or a missing argument, is undefined behavior (fprintf on cppreference).

A format string built at run time

std::println(fmt, port) with a std::string fmt does not compile (Clang: read of non-constexpr variable 'fmt' is not allowed in a constant expression). Pass the string to std::vprint_unicode with stdout first, the form that built on all four:

[Example 10:
std::string fmt = "port {}\n";   // known only at run time
std::vprint_unicode(stdout, fmt, std::make_format_args(port));

std::string bad = "host {:d}\n";   // d is for integers
try {
    std::vprint_unicode(stdout, bad, std::make_format_args(host));
} catch (const std::format_error& e) {
    std::println("format_error: {}", e.what());
}

— end example]

The check moves to run time, as a std::format_error. On Microsoft’s STL it printed:

[Example 11:
port 8080
host format_error: Invalid presentation type for string

— end example]

libstdc++ also wrote host before it threw; libc++ wrote nothing. [print.syn] also declares std::vprint_unicode(fmt, args) without the stream, but only libc++ built it.

In C++26, std::println(std::dynamic_format(fmt), port) takes the string directly. The name comes from P3953R3, voted into C++26 in March 2026; until then it was std::runtime_format, so the name a release accepts in C++26 mode depends on its library:

Library Name in C++26 mode
libstdc++ std::runtime_format in GCC 14 and 15; std::dynamic_format from GCC 16.1
libc++ std::runtime_format in 18 to 22; std::dynamic_format from 23
Microsoft’s STL neither (MSVC 19.44, Clang 19.1)

GCC 16.2 and Clang 23.1 with libc++ built std::dynamic_format under -std=c++26 and rejected std::runtime_format. std::vprint_unicode(stdout, fmt, std::make_format_args(…)) is C++23 and built on all four, whatever the name.

What std::println can print

On all four compilers:

Argument {} printed
int 8080 8080
double 0.4567 0.4567
bool true true; {:d} gives 1
char ':' :; {:d} gives 58
std::string, std::string_view db.local, https
const char* the text: db.local
nullptr 0x0
void*, const void* the address in hex: 0x1f90
int* does not compile
std::vector<int> [8080, 8443]
std::pair{host, port} ("db.local", 8080)
std::map<std::string, int> {"db.local": 8080}
std::chrono::seconds(90) 90s
A struct of yours does not compile without a std::formatter

Ranges print in C++23 through P2286R8, with strings inside quoted. For &port, Clang names the missing formatter<int *, char>: among pointers, only void*, const void* and std::nullptr_t print an address (char* and const char* print the string they point to), so write static_cast<const void*>(&port).

Your own type: the smallest std::formatter

[Example 12:
struct Endpoint {
    std::string host;
    int port;
};

template <>
struct std::formatter<Endpoint> {
    constexpr auto parse(std::format_parse_context& ctx) {
        return ctx.begin();   // no specs: only {}
    }
    auto format(const Endpoint& e, auto& ctx) const {   // any context
        return std::format_to(ctx.out(), "{}:{}", e.host, e.port);
    }
};
// ...
Endpoint db{"db.local", 8080};
std::println("{}", db);                    // db.local:8080
std::vector<Endpoint> all{db, {"cache.local", 6379}};
std::println("{}", all);                   // C++23: a range of them

— end example]

It printed:

[Example 13:
db.local:8080
[db.local:8080, cache.local:6379]

— end example]

Only parse has to be constexpr; without it, all four rejected the call. Take the context as auto& ctx: with std::format_context& ctx, the struct printed but a std::vector<Endpoint> did not compile on any of the four. The range formatter requires formattable<Endpoint, char>, which calls format with a context whose iterator type is unspecified ([format.formattable]), and a format that takes only std::format_context& fails that test.

std::endl vs ‘\n’: what the flush is

std::endl writes '\n' and then flushes the stream: the standard defines it as os.put(os.widen('\n')) followed by os.flush() ([ostream.manip]). '\n' writes the character and nothing else. A stream buffer that counts its sync() calls shows the difference:

[Example 14:
// A stream buffer that counts its flushes: a flush ends in sync().
class FlushCounter : public std::stringbuf {
public:
    int flushes = 0;

protected:
    int sync() override {
        ++flushes;
        return std::stringbuf::sync();
    }
};
// ...
FlushCounter buf;
std::ostream out(&buf);

for (int i = 0; i < 1000; ++i)
    out << "port " << port << std::endl;
// ...
for (int i = 0; i < 1000; ++i)
    out << "port " << port << '\n';
// ...
out << std::flush;

— end example]

All four printed:

[Example 15:
1000 lines with std::endl: 1000 flushes
1000 lines with '\n':     0 flushes
one std::flush:            1 flush

— end example]

Three stages: your program, the stream’s buffer as cells, terminal or file. A newline arrow runs from the program to the buffer and stops: no flush. A std::endl arrow reaches the buffer, then a bold flush arrow reaches the terminal.
Figure 2 — A newline goes into the buffer; std::endl also empties the buffer.

The counts are calls to the stream buffer’s sync(), not what reached the screen. On a terminal, C’s stdout is not fully buffered (C rules that out for an interactive device, and on Linux it is line-buffered), so with the default synchronization a std::cout line ended with '\n' still appears at once. The flush you save is the one a file or a pipe would otherwise get after every line: a std::ofstream file held 0 bytes after a '\n' line, and 20 once a std::endl line followed.

  • When you want the flush (a progress line, a message before a crash), name it: std::cout << std::flush writes no newline.
  • std::cout is flushed anyway before a read from std::cin and before each output to std::cerr, both tied to it, and when the program ends normally.
  • std::cerr has unitbuf set, so it flushes after every output.

SL.io.50 of the Core Guidelines puts it this way: “The endl manipulator is mostly equivalent to '\n' and "\n"; as most commonly used it simply slows down output by doing redundant flush()s.” Neither writes \r\n: on Windows a text-mode file turns the '\n' into \r\n, and the same guideline notes that “endl does not take care of producing a platform specific end-of-line sequence (like "\r\n" on Windows)”.

Visual Studio’s clang-tidy 19.1.5 reported warning: do not use 'std::endl' with streams; use '\n' instead [performance-avoid-endl] for a std::endl on std::cout and one on std::cerr, rewrote both to '\n' and left std::flush alone. It is among the analyzers on the Toolchain page.

Does std::println flush, and is it thread-safe?

It depends on the overload, the one that takes a FILE* or the one that takes a stream:

Overload Flushes Locks
To a FILE* (stdout, stderr, a file) No, except before writing to a terminal through a native Unicode API, such as the Windows console ([print.fun]) Yes, since P3107R5: another thread’s output through the same FILE* does not land inside the line
To a stream (std::cout, std::cerr) As any << does: its tie() first, then the stream itself when unitbuf is set ([ostream.formatted.print]) No
  • So std::println(std::cerr, …) flushes std::cout and then std::cerr.
  • P3107R5 (2024) is in C++26 and meant to be applied to C++23 too. Its lock keeps out output through the same FILE* only, not through another FILE* on the same file.
  • Counted on all four: 1,000 println calls to a plain std::ostream made 0 flushes; with unitbuf set, 1,000; to a stream tied to another, 1,000 flushes of the other and 0 of its own.
  • A line written to a FILE* was not on disk (0 bytes) until std::fflush (10 bytes).
  • Two threads writing 2,000 lines of 200 characters each to one FILE* gave 4,000 whole lines on all four.

std::println takes the Unicode path only when the ordinary literal encoding is UTF-8; on MSVC that takes /utf-8 (or /execution-charset:utf-8), which every MSVC build here used.

What std::println needs: compilers, libraries, flags

It needs C++23 mode and <print>. The first releases, from Since When: GCC 14.1, Clang 18 and Visual Studio 2022 17.7. Beyond that:

  • Clang 18 means libc++ 18 (-stdlib=libc++). Clang on Linux uses libstdc++ by default, and there it needs GCC 14’s library.
  • Range formatting came later in two libraries: libstdc++ 15.1 (complete in 15.2) and Visual Studio 2022 17.12. libc++ formats ranges since 16, before its <print> in 18.
  • GCC 14’s release notes say std::print on Windows needs -lstdc++exp (GCC 14 changes).
  • In C++23 mode, __cpp_lib_print was 202406 on Microsoft’s STL and libstdc++: they have P3107R5 and P3235R3, which print straight into the locked stream and opt more standard types in. libc++ 23.1 reported C++23’s 202207.

Under C++20 (-std=c++20, /std:c++20), each build said:

Build What it said
MSVC 19.44 warning STL4038: The contents of <print> are available only with C++23 or later.
Clang 19.1, Microsoft’s STL error: use of undeclared identifier 'std' (with <print> the only header)
GCC 16.2 note: 'std::println' is only available from C++23 onwards
Clang 23.1, libc++ error: no member named 'println' in namespace 'std'; did you mean 'printf'?

std::cout << std::format("{}:{}\n", host, port); built under C++20 on all four; std::format itself ships from GCC 13.1, Clang 14 and Visual Studio 2019 16.10 (Since When’s C++20 table).

Which one to use: std::println, std::cout or printf

Situation Write Since
New code std::println("port {}", port) C++23
C++20, no <print> std::cout << std::format("{}:{}\n", host, port) C++20
Older code std::cout << "port " << port << '\n' every standard
An error message std::println(stderr, …), or std::cerr << C++23; every standard
Text that must show now std::cout << std::flush, or std::fflush(stdout) after std::println every standard
A format string built at run time std::vprint_unicode(stdout, fmt, std::make_format_args(…)) C++23
printf calls you inherit leave them, or let clang-tidy rewrite them clang-tidy, C++23

The Core Guidelines still say “Prefer iostreams for I/O” (SL.io.3), a rule written before <print> existed.

clang-tidy’s modernize-use-std-print does the rewriting under -std=c++23. On a sample of printf calls and std::endl lines, clang-tidy 19.1.5:

  • reported warning: use 'std::println' instead of 'printf' [modernize-use-std-print], or 'std::print' for the line with no \n;
  • turned fprintf(stderr, …) into std::println(stderr, …), dropped .c_str() and added #include <print>;
  • left int n = std::printf(…) alone, because its result is used.

Each rewrite printed what the original printed, on all four; for a bool and a char that takes {:d}, not std::println’s default:

[Example 16:
std::printf("verbose %d\n", verbose);
std::println("verbose {:d}", verbose);   // {:d}: 1, not true
std::printf("sep %d\n", sep);
std::println("sep {:d}", sep);           // {:d}: 58, not :

— end example]

Reading a number back out of text is the reverse job: C++ string to int with std::stoi or std::from_chars. The other answers on the library are under Standard library.

Questions

What does using std::cout; do?

It is a using-declaration: it brings the one name std::cout into the current scope, so cout << "port " << port << '\n'; compiles without the std:: prefix. using namespace std; makes every name in std visible instead. Either way, std::cout itself comes from #include <iostream>.

Why do I get “undefined reference to std::cout”?

The program was linked without the C++ standard library. Linking with gcc does that: GCC’s manual says “the use of gcc does not add the C++ library”, while g++ “automatically specifies linking against the C++ library”. Link with g++. With libc++, the same error names std::__1::cout.

What does std::flush do?

It flushes the stream: the standard defines it as a call to os.flush(), and it writes no newline. std::endl is a newline followed by the same flush. On a stream buffer that counted its flushes, one std::flush made one, 1,000 std::endl lines made 1,000 and 1,000 lines ended with ‘\n’ made none.

How do I print a std::vector in C++?

In C++23, std::println("{}", v) prints a vector of ints as [8080, 8443], through the range formatting of P2286R8; MSVC 19.44, GCC 16.2 and Clang 19.1 and 23.1 all printed that. The first releases where it works are libstdc++ 15.1 (complete in 15.2), libc++ 18 (the first with <print>; it formats ranges with std::format since 16) and Visual Studio 2022 17.12. Before C++23, loop over the elements and print each one.

Is there a std::println for wide strings?

No. <print> declares std::print and std::println for char format strings only, so an L"…" format string or a std::wstring does not go through them. A std::string holding UTF-8 text does, and it takes the Unicode path to a terminal when the ordinary literal encoding is UTF-8.

Read next

  1. std::expected in C++23: return a value or an errorStandard Library · Miriam Castellanos · written for C++23
  2. C++ int to string: to_string, format or to_chars, not itoaStandard Library · Miriam Castellanos · written for C++17
  3. std::optional in C++: how to use it and when not toStandard Library · Miriam Castellanos · written for C++17