I'm looking to get some feedback on a small printf
-like function I wrote that provides simplified behavior similar to boost::format
:
#ifndef EXT_FORMAT_HPP__
#define EXT_FORMAT_HPP__
// ext::format
// Implements a variadic printf-like function providing behavior similar to
// boost::format
#include <regex>
#include <sstream>
#include <string>
#include <utility>
namespace {
inline std::string format_helper(
const std::string &string_to_update,
const size_t) {
return string_to_update;
}
template <typename T, typename... Args>
inline std::string format_helper(
const std::string &string_to_update,
const size_t index_to_replace,
T &&val,
Args &&...args) {
std::regex pattern{"%" + std::to_string(index_to_replace)};
std::string replacement_string{(std::ostringstream{} << val).str()};
return format_helper(
std::regex_replace(string_to_update, pattern, replacement_string),
index_to_replace + 1,
std::forward<Args>(args)...);
}
} // namespace
namespace ext {
template <typename... Args>
inline std::string format(const std::string &format_string, Args &&...args) {
return format_helper(format_string, 1, std::forward<Args>(args)...);
}
} // namespace ext
#endif
Usage example:
#include "format.hpp"
#include <iostream>
struct foo {
int value;
foo(int val) : value{val} {
}
};
std::ostream &operator<<(std::ostream &os, const foo &f) {
os << "foo(" << f.value << ")";
return os;
}
int main() {
double tmp = 37.382;
std::cout << ext::format("%1 + %2 * %1 = %3", 5, tmp, 5 + tmp * 5) << std::endl;
// Support user defined types provided the appropriate operator<< overload is defined.
foo a_foo(55);
std::cout << ext::format("Here is a foo constructed with 55: %1", a_foo) << std::endl;
};
This is not written to replace boost::format
; it's intended to be used as a header-only include with a very limited scope. As currently written, it is possible to supply too few or too many arguments to the parameter pack. Passing too few arguments will return a string that still has format specifiers. Passing excess arguments are simply ignored and you pay the price of excess calls to std::regex_replace
, which will do nothing. With that in mind, I'm looking for any and all feedback.
2 Answers 2
I find this to be a sweet demo, but I don't like the traps in it for the unwary. Like you mention, there is little validation of parameter counts. There is also a chance that the same location can be replaced multiple times, e.g. with a call to ext::format("%1", "%2", "%3, "%4", "hi!")
resulting in "hi!"
. Perhaps you might consider that a bonus.
What I find most surprising in the code is this:
std::string replacement_string{(std::ostringstream{} << val).str()};
I would probably have tended towards auto replacement_string{std::to_string(val)};
or, more likely, just embedded that in the call to regex_replace
. Do you do this for compatibility with existing code that provides an overload for operator<<
but not to_string
?
Finally, I'm torn about the use of regex
. It seems like a pretty big hammer without a lot of need, here, however it does keep the code short and sweet, and implicitly avoids an infinite loop that a flawed implementation might have with ext::format("%1", "%1")
.
Reviewing the tests, I find the Does_Not_Alter_Format_String
test to be surprising as well. I would have thought the const
qualifier would indicate this well enough.
-
\$\begingroup\$ I didn't know about
std::to_string
overloading! I did it primarily to support any class that overloadsoperator<<
. Thank you for the feedback! \$\endgroup\$countfromzero– countfromzero2013年12月10日 04:13:26 +00:00Commented Dec 10, 2013 at 4:13 -
\$\begingroup\$ I believe it would be possible to just call
to_string(val)
, and provide a templateext::to_string()
that uses<<
, for those classes without their ownto_string()
. \$\endgroup\$Toby Speight– Toby Speight2020年01月17日 09:07:19 +00:00Commented Jan 17, 2020 at 9:07
FTBFS
This code doesn't compile here (GCC 9.2, -std=c++2a):
29935.cpp:24:64: error: ‘class std::basic_ostream<char>’ has no member named ‘str’
24 | std::string replacement_string{(std::ostringstream{} << val).str()};
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^~~
I had to expand that to keep a reference to the concrete class:
std::ostringstream stream{};
stream << val;
std::string replacement_string{stream.str()};
Major
We can pass %%
to std::sprintf()
to produce a single %%
, but that doesn't work here, so there's no way for the format string to specify a literal %1
to output.
Very minor
There's a stray ;
after the definition of main()
in the test program.
format_helper
for clarity rather than trying to squeeze everything into the return call. I also renamed variables to be more descriptive and reduced all line lengths to be within 80 characters. \$\endgroup\$