-
Notifications
You must be signed in to change notification settings - Fork 36
Description
This is an idea I had a few years back. I've gone back and forth on it a lot (I'm torn about whether the benefits outweigh the negatives... it seems a little close). I figured that I would throw it out there to see what people think
The problem
Using data to format strings in C++ is unsatisfactory. Historically, there are 2 approaches:
- use c-style
printf
-functions. (what we currently do) - use C++-style streams like
std::cout
orstd::cerr
Of these 2 options I'm generally a fan of the first since it is far less verbose and the data-formatting syntax is fairly "standard". However, the major drawback is that it can't properly handle any non-C primitives (e.g. it handles const char*
strings, but not std::string
or a user-defined custom string implementation).
The potential solution
The potential solution to is to make use of fmt
C++ library (docs and GitHub repository). This is an extremely popular library that takes a printf
-style approach to string formatting. In fact, it is so popular, that it has become part of the standard library (most features were added in C++20, a few more introduced in C++23 and C++26).
The basic premise is that it performs formatting based on python's str.format(*arg, **kwargs)
formatting string. As a basic example, if one wrote
std::string s = fmt::format("This is a float {:.3f} and this is an int {}", 42.39542342, 42);
Then s
would hold "This is a float 42.395 and this is an int 42"
.
Importantly, it provides:
- support for formatting types in the standard library. This includes containers like
std::vector
. It also includesstd::string
(and supportingstd::string
is extremely important)1 - a mechanism to allow formatting of custom types (which could help with debugging).
- a
fmt::format_to
function that can be used to avoid heap allocations.
I think a major advantage is that in 3 to 6 years (whenever we update to future C++ releases), we could eventually remove fmt
as a dependency and use the standard library instead (just like we did with boost::filesystem
).
Drawbacks
The primary drawbacks is compilation times. I'm most concerned about this. Some compile-time and binary-size benchmarks were done (here). In that benchmark fmt
takes 3 times longer to compile than printf
and produces larger binaries. With that said, at runtime these other benchmarks suggest that fmt
is generally ~20% faster than printf
and an order of magnitude faster at formatting floats.
In theory, another drawback is that we're introducing a new dependency. However, I don't think this is a problem in practice.2
Summary
Again, I'm really torn on this. I think it all boils down to the question: "will we use this feature when we transition to C++20 and it becomes part of the standard library?"
- If the answer is yes, then I think we should do it (since it will reduce the burden of transitioning new code added between now and then AND we get to enjoy the new feature now).
- I tend to think that we want to use this feature -- I think it's hard to overstate the value in making it easy to write descriptive error messages (if it's difficult, then people are less likely to write them OR they may not make them descriptive).
I definitely want to hear people's opinions on this topic!
Footnotes
-
Yes
std::string
has the.c_str()
method which lets it work with normalprintf
. However, there is some "messiness" with making sure that thestd::string
is alive over the course of theprintf
-related function, that comes up when you call.c_str()
on a temporarystd::string
that hasn't been stored in a variable (e.g. iffoo()
allocates and returns a string, then callingfoo().c_str()
may have some weird behavior). I believe that this "messiness" invokes undefined behavior (that may produce desired results on some platforms but not on others) ↩ -
Since
fmt
is small & lightweight and makes use of cmake (with modern practices), we could make Enzo-E's build have the default behavior of automatically downloading and installing it as a part of the build process. In other words, the end-user doesn't need to do anything. The only other dependency-related concern is compiler support (e.g. maybe a particular compiler has issues with it). Since this only uses standard C++ features this shouldn't be a problem. Plus I imagine that any compiler issues have been sorted out given the immense popularity of this library. ↩