Why you can't std::format a smart pointer (and how to anyway)
A question went around r/cpp recently (credit to u/SPEKTRUMdagreat) that is worth answering properly, because the answer reveals a deliberate design choice rather than a gap. The setup: streaming a smart pointer works,
std::cout << "uptr: " << u_ptr << '\n'; // fine
std::cout << "sptr: " << s_ptr << '\n'; // fine
but formatting one does not:
std::println("uptr: {}", u_ptr); // ill-formed: no formatter<unique_ptr<int>>
std::println("sptr: {}", s_ptr); // ill-formed: no formatter<shared_ptr<int>>
std::cout << u_ptr compiles because operator<< overloads exist for shared_ptr (since C++11) and unique_ptr (since C++20); they print the managed object’s address. std::format has no matching std::formatter, so the second snippet does not compile. The question: is that an oversight?
It is deliberate, not an oversight
std::format is conservative about pointers on purpose. Among all pointer types, only void*, const void*, and nullptr_t are formattable. An arbitrary int* is not. The formatter for it is disabled, so you cannot accidentally print an address when you meant a value. A unique_ptr<int> formatter would have to punch a hole in exactly that rule.
And it is not that nobody thought of it. The original proposal that gave std::format its library formatters, P1636 “Formatters for library types” (Dimov/Vollmann, 2019), did include shared_ptr and unique_ptr (formatting .get() cast to void*). LEWG asked for the smart pointers to be removed. The exclusion has held: the follow-up P2930 “Formatter specializations for the standard library” adds formatters for optional, variant, expected, and any, and still leaves smart pointers out.
Why is that defensible? Because “format a smart pointer” has no single obvious meaning. The address (what operator<< prints) is rarely what you want and is non-reproducible. It changes every run under ASLR, which makes it poison for golden-output tests. The pointee is usually what you mean, but that needs the pointee to be formattable and needs null handled. Faced with two reasonable answers and a footgun, the committee declined to guess. The streaming operators predate that philosophy; std::format started clean and stayed strict.
So: not a thing you are missing. Here are the three ways to do it today.
1. The address, like operator<< (plus a C++26 upgrade)
Cast .get() to void* yourself. That is the one pointer type std::format accepts. C++26’s P2510 “Formatting pointers” then lets you dress that void* up directly (uppercase, zero-padding) without the old reinterpret-to-integer trick:
const void* p = static_cast<const void*>(u.get()); // arbitrary int* is not formattable
std::println("plain: {}", p); // 0xcafef00d
std::println("upper: {:P}", p); // 0XCAFEF00D (C++26)
std::println("zero-pad: {:018}", p); // 0x00000000cafef00d (C++26)
2. The pointee, which is usually what you want
Most of the time the address is noise and you want the value. Dereference and format that, guarding against null:
auto show = [](const auto& q) {
return q ? std::format("{}", *q) : std::string("(null)");
};
std::println("p -> {}", show(p)); // p -> 42
std::println("n -> {}", show(n)); // n -> (null)
3. Write the formatter the standard left out
If you want {} to work directly on the pointer, supply the std::formatter yourself. One catch worth knowing: you may only add a specialization to a std template when a program-defined type is involved. std::formatter<std::unique_ptr<Point>> is fine because Point is yours; std::formatter<std::unique_ptr<int>> would be non-conforming. That is also the realistic case, since you want to print unique_ptr<YourType>, not unique_ptr<int>:
template <class CharT>
struct std::formatter<std::unique_ptr<Point>, CharT> {
constexpr auto parse(auto& ctx) { return ctx.begin(); }
auto format(const std::unique_ptr<Point>& p, auto& ctx) const {
if (p) return std::format_to(ctx.out(), "Point({}, {})", p->x, p->y);
return std::format_to(ctx.out(), "(null)");
}
};
std::println("{}", p); // Point(3, 4)
If you are already on {fmt}, fmt::ptr(p) does the void*-address version for you, smart pointers included.
The takeaway
The asymmetry between operator<< and std::format marks a line that std::format holds and operator<< never did: pointers are not formattable unless you say void* and mean it. Smart pointers were considered, removed, and have stayed removed across two proposals. If a standard formatter for them ever lands, expect it to print the void* address and to be gated exactly like the old operator<<. That is the one of the three options above you can already write in a single cast.
Sources: P1636R2, Formatters for library types (smart pointers proposed, then removed by LEWG). P2930R0, Formatter specializations for the standard library (adds optional/variant/expected/any; not smart pointers). P2510R3, Formatting pointers (C++26 void* formatting: {:P}, padding). The void*-only rule is in the [format] wording at eel.is/c++draft/format. Examples verified on Compiler Explorer with GCC 16.1 and the Bloomberg clang-p2996 fork.