I think the original point was to make a typesafe printf that's still reasonably efficient without creating a bunch of small temporary strings. Like, if you printf like this:
printf("Number %d, String %s", n, s);
but the types of n and s aren't int and char*, all hell breaks loose and you have the origin of a million CVEs. But how do you make that function signature typesafe, without the tools of modern templates? You sort of can't. One thing you can do is do string append stuff, but the syntax then is annoying:
This also creates a bunch of temporary strings, which is not ideal (and still relies on operator overloading, btw). In this world, using operators for this does make some sense:
std::cout << "Number: " << n << ", String " << s;
Like, seen from this perspective, it's not the worst idea in the world, it does work nicely. The syntax really isn't too bad, IMHO (the statefulness part, though, really sucks). It also allows you to add formatting for your own types: just overload operator<<!
Clearly, properly type-safe std::format is vastly superior, but the C++ of the 90s simply didn't have the template machinery to make that part of the standard library.
And there are ways of signaling to a compiler that your function accepts a format string and a series of typed arguments in case you're wrapping a C-style printf function.
That's true, however neither the mechanism to do this in I/O streams, nor the weighty mechanism for std::format come close to what Rust's derive macro offers for Debug, and in my experience asking for so much means you often won't get anything because programmers are lazy.
That is, for a custom type in Rust having rudimentary Debug output is one word added to the list of derive macros, the word "Debug". Almost everybody will write that, it's barely effort.
To get that in C++ I/O Streams you need to write an operator overload function and must be careful to write it correctly. Trivial types usually don't bother, but many C++ programmers are comfortable writing this for an important type.
For std::format you need to write a template which is already pretty scary, and then it needs to implement custom parse and format steps, this is because std::format allows your custom type to define its own custom formatting rules. This is extremely powerful, but everybody must pay for this power. I have not seen most people bother doing all this at all.
You can write operator<< as a free (non-member) function yourself for classes that don’t implement it. Furthermore, it’s not necessarily the same output that you want for debugging.