I'm often getting the impression that these "you're a fool using these well-known but insecure libraries. Better use this new shiny thing because it's safe" discussions are a bit removed from reality.
But I'm asking in earnest. Please also check out my benchmark in the sibling thread where I compared stringstream with stdio/snprintf build performance. Would in fact love to compare std::format_to_n, but can't be arsed to put in more time to get it running right now.
I don't know about those specifically right now, but in general these things have huge compile time costs and are also generally less ergonomic IMO. [EDIT: cobbled together a working version and added it to my test below, see Version 0].
> About "sigh of despair" and "sigh of relief": Are you expressing concern about the template function signature?
Yes. It's a mouthful, and I'm worried not only about static checks but about other things too -- like readability of errors, include & link dependencies, compile performance, amount of compiled code (which is minimal in case of snprintf/varargs)... I would need to check out std::format_to_n() as suggested by the sibling commenter.
And hey -- snprintf has been available for easily 30+ years ... while the and headers that people make such a fuss about, don't even seem available on gcc nor clang on my fully updated debian bookworm system. The reason is that those implementations aren't complete, even though is C++20. The recommended way to get those headers is to grab https://github.com/fmtlib/fmt as an external library... Talk about the level of hype and lack of pragmatism that's going on around here. People are accusing each other for not using a standard library that isn't even implemented in compilers... And with a likelyhood they haven't used the external library themselves, and given that this library is external it's not heavily tested and probably contains bugs still, maybe CRASHES and SECURITY EXPLOITS.
But let me test C++ features that actually exist:
#if VERSION == 0
#include
#include
struct membuf: std::streambuf
{
membuf(char *p, size_t size)
{
setp(p, p + size);
}
size_t written() { return pptr() - pbase(); }
};
int main()
{
char buffer[256];
membuf sbuf(buffer, sizeof buffer);
std::ostream out(&sbuf);
out << "Hello " << 42 << "\n";
fwrite(buffer, 1, sbuf.written(), stdout);
return 0;
}
#elif VERSION == 1
#include
#include
void test(std::stringstream& os)
{
os << "Hello " << 42 << "\n";
}
int main()
{
std::stringstream os;
test(os);
std::cout << os.str();
return 0;
}
#elif VERSION == 2
#include
int test(char *buffer, int size)
{
int r = snprintf(buffer, size, "Hello %d\n", 42);
return r;
}
int main()
{
char buffer[256];
int len = test(buffer, sizeof buffer);
fwrite(buffer, 1, len, stdout);
return 0;
}
#endif
Compile & link: CT LT TT PT PL
-DVERSION=0 0.361s 0.095s 0.456s 0.081s 32573
-DVERSION=1 0.364s 0.089s 0.453s 0.074s 32338
-DVERSION=2 0.039s 0.088s 0.127s 0.031s 918
CT=compile time, LT=link time, TT=total time (CT+LT), PT=preproc time (gcc -E), PL=preprocessor output linesBench script:
# put -DVERSION=1 or -DVERSION=2 as cmdline arg
time clang++ -c "$@" -Wall -o test.o test.cpp
time clang++ -Wall -o test test.o
time clang++ "$@" -Wall -E -o test.preprocessed.txt test.cpp
wc -l test.preprocessed.txt
My clang version here is 14.0.6. I measured with g++ 12.2.0 as well and the results were similar (with only 50% of the link time for the snprintf-only version).For such a trivial file, the difference is ABYSMAL. If we extrapolate to real programs we can assume the difference in build times to be 5-10x longer for a general change in programming style. Wait 10 seconds or wait 1 minute. For a small gain in safety, how much are you willing to lose? And how much do this lost time and resources actually translate to working less on the robustness of the program, leaving more security problems (as well as other problems) in there?
And talking about lost run time performance, that is real too if you're not very careful.
> For snprintf(), how do you ensure that your format string and variadic arguments will not cause crash at runtime? The C++ version is compile-time type safe.
Honestly I just don't ensure it perfectly -- beyond running them once as described. I write a lot of code that isn't fully proofed out from the beginning. Exploratory code. A few printfs are really not a concern in there, there are much bigger issues to work out.
I also absolutely do have some printfs that were quickly banged out but that are hidden in branches that have never actually run and might never happen -- they were meant for some condition that I'm not even sure is possible (this happens frequently when checking return values from complicated APIs for example).
The real "problem" isn't that there is a possibly wrong printf in that branch, but that the branch was never tested, and is likely to contain other, much worse bugs. But the fact that the branch was never run also means I don't care as much about it, pragmatically speaking. Likely there is an abort() or similar at the end of the branch anyway. It's always important to put things into perspective like that -- which is something that seems often missing from C++ and similar cultures.
The more proofed out some code gets, the more scrutiny it should undergo obviously.
Apart from that, compilers do check printfs, and I usually get a warning/error when I made a mistake. But I might not get one if I write my own formatting wrappers and am too lazy to explicitly enable the checking.
print/println are based on the fmt library (https://github.com/fmtlib/fmt), which had it's first release in 2015, so it's roughly as old as Rust afaik. It's mainly inspired by Python formatting.
Having per-type formatters is just a logical conclusion of having type-safe formatting.
iostreams are for all kinds of io (which includes stdin/stdout), while fmt is entirely dedicated to formatting strings. Those things are related, but not the same. cout and cerr and such will probably be entirely superseeded by print/println (I hope), but it doesn't make iostreams generally redundant.
println adds a newline and you want to be able to choose, so there is print and println.
and this could be also handy: https://en.cppreference.com/w/cpp/meta
https://github.com/rigtorp/awesome-modern-cpp
The list includes some powerful yet not excessively large libraries from which one can learn including:
https://github.com/skypjack/entt
There's nuance to banning that since implementating multiple interfaces is technically multiple inheritance in C++. So I think I'd agree with you but with an exception for pure-virtual classes.
> Also, while iostream is a bit of a mess, having it be strongly typed means that it's, in my opinion, well worth using over cstdio.
Orthodox C++ would ban this but you can have both printf-style with type safety with https://github.com/fmtlib/fmt
Which inspired C++20's std::format: https://en.cppreference.com/w/cpp/utility/format
Which is also banned by Orthodox C++
What if you, as library author (not compiler writer!), think of a much better formatting system? Or something that's maybe not better in general, but better for an unusual situation you're deploying in? Or you want to write something that's different to a formatting system but uses similar tricks? If you don't have language facilities to do this yourself then you're stuffed.
As well the original post showing it's possible in Rust, this is also possible in C++. For example, the C++ fmt [1] library allows compile-time time checking of the number of slots in format strings against the number of arguments, and even that the format specifications (.03d or whatever) match the passed in types. And it's not reliant on the compiler doing its own magic checking outside of the language.
1. ospan, performance claims seem to be based on, doesn't do any bound checks, so you can easily get buffer overflow.
2. fast_io generates a whopping 50kB of static data just to format an integer.
So if these benchmark results are correct (I was not able to verify because the author hasn't provided the benchmark source):
> format_int 7867424 ns 7866027 ns 89 items_per_second=127.129M/s
> fast_io_ospan_res 6871917 ns 6870708 ns 102 items_per_second=145.545M/s
fast_io gives 15% perf improvement by replacing a safe format_int API from https://github.com/fmtlib/fmt with a similar but unsafe one + 50kB of extra data. Adding safety will likely bring perf down which the last line seems to confirm:
> fast_io_concat 7967591 ns 7966162 ns 88 items_per_second=125.531M/s
This shows that fast_io is slightly slower than the equivalent {fmt} code. Again this is from the fast_io's benchmark results that I hasn't been able to reproduce.
50kB may not seem like much but for comparison, after a recent binary size optimization, the whole {fmt} library is around 57kB when compiled with `-Os -flto`: http://www.zverovich.net/2020/05/21/reducing-library-size.ht...
The floating-point benchmark results are even less meaningful. They appear to be based on a benchmark that I wrote to test the worst case Grisu (https://www.cs.tufts.edu/~nr/cs257/archive/florian-loitsch/p...) performance on unrealistic random data with maximum digit count. fast_io compares it to Ryu (https://dl.acm.org/doi/pdf/10.1145/3192366.3192369) where maximum digit count is actually the best case and the performance degrades as the number of digits goes down. A meaningful thing to do would be to use Milo Yip's benchmark instead: https://github.com/miloyip/dtoa-benchmark
With iostreams there are no format strings so there is nothing to validate. Libraries such as https://github.com/fmtlib/fmt are allow to do compile-time parsing of the string though.
Both libs don't fix the underlying problems of std::string, but at least they reduce the pain of working with strings in C++ considerably.
auto for_each = [](R&& r, Fun fun) {
return r | view::transform(fun) | view::join;
};
similarly, it seems that yield_if can also be radically simplified auto yield_if = [](bool b, T x) {
return b ? view::single(x)
: view::empty;
};
But if you actually read the blog post... for_each and yield_if are a part of the range_v3 library and are proposed additions to the language.Also I don't know what the heck is up with that cout statement; we can use structured declarations to make that look more pythonic too. And there's also a proposal for string formatting [1]
#include "fmt/format.h"
#include
#include
using namespace std;
using namespace view;
using namespace fmt;
int main() {
auto triples =
for_each(iota(1), [](int z) {
return for_each(iota(1, z+1), [=](int x) {
return for_each(iota(x, z+1), [=](int y) {
return yield_if(x*x + y*y == z*z,
make_tuple(x, y, z));
});
});
});
for(auto [x,y,z] : triples | take(10))
cout << format("{}, {}, {}", a, b, c) << endl;
}
So despite all the protests about how ugly Eric's solution is... I don't think the above is significantly worse than your python & rust equivalents. Speaking as a python evangelist who uses C++ out of nece$$ity.Based on the article, I think fmtlib will be faster than what they did here.