I am learning C++ and currently playing with variadic template functions and formatting string, So here is my small attempt to write a formatting function and provide a simple iostream-like interface for std::FILE, I am using '%' as a format specifier f.e
format("const CharT *str") == "const CharT *str"
format("HelloWorld - %", 10) == "HelloWorld - 10"
here is the code:
#include <cstdlib>
#include <string>
namespace gupta {
using string_t = std::string;
using CharT = string_t::value_type;
inline string_t format(const CharT *str) { return str; }
template <typename T> inline string_t to_string(const T &f) { return std::to_string(f); }
inline string_t to_string(const char *str) { return std::string{str}; }
inline string_t to_string(string_t s) { return (s); }
template <typename T, typename... Ts>
string_t format(const CharT *str, const T &arg, const Ts &... args) {
string_t res;
for(; *str; str++) {
if(*str == '%') {
if(*(str + 1) == '%') {
str++;
}
else {
res += to_string(arg);
return res += format(str + 1, args...);
}
}
res += *str;
}
return res;
}
template <typename... Ts> auto fprint(std::FILE *f, const char *str, Ts &&... args) {
auto s = std::move(format(str, std::forward<Ts>(args)...));
return fwrite(s.data(), 1, s.size(), f);
}
template <typename... Ts> inline auto print(const char *str, Ts &&... args) {
return fprint(stdout, str, std::forward<Ts>(args)...);
}
template <typename... Ts> inline auto debug(const char *str, Ts &&... args) {
return fprint(stderr, str, std::forward<Ts>(args)...);
}
namespace detail {
class _stdout_object {};
class _stderr_object {};
} // namespace detail
namespace printing_shortcuts {
template <typename T> detail::_stdout_object operator<<(detail::_stdout_object f, const T &arg) {
auto s = to_string(arg);
fwrite(s.data(), 1, s.size(), stdout);
return f;
}
template <typename T> detail::_stderr_object operator<<(detail::_stderr_object f, const T &arg) {
auto s = to_string(arg);
fwrite(s.data(), 1, s.size(), stderr);
return f;
}
detail::_stdout_object print() { return {}; }
detail::_stderr_object debug() { return {}; }
} // namespace printing_shortcuts
using namespace printing_shortcuts;
} // namespace gupta
using namespace gupta::printing_shortcuts;
class test {};
std::string to_string(const test &) { return "test"; }
#include <assert.h>
int main() {
using namespace gupta;
assert(format("const CharT *str") == "const CharT *str");
assert(format("HelloWorld - %", 10) == "HelloWorld - 10");
print("%s\n", format("HelloWorld - %,%", 10, test{}));
assert(format("HelloWorld - %,%", 10, test{}) == "HelloWorld - 10,test");
}
also, does it make sense to replace class _stdout_object
while their pointers with something like
namespace detail {
class _stdout_object;
class _stderr_object;
} // namespace detail
namespace printing_shortcuts {
template <typename T> detail::_stdout_object *operator<<(detail::_stdout_object *, const T &arg) {
auto s = to_string(arg);
fwrite(s.data(), 1, s.size(), stdout);
return nullptr;
}
template <typename T> detail::_stderr_object *operator<<(detail::_stderr_object *, const T &arg) {
auto s = to_string(arg);
fwrite(s.data(), 1, s.size(), stderr);
return nullptr;
}
detail::_stdout_object *print() { return nullptr; }
detail::_stderr_object *debug() { return nullptr; }
} // namespace printing_shortcuts
2 Answers 2
Don't make unnecessary type aliases
using string_t = std::string;
using CharT = string_t::value_type;
Those neither add readability nor make the existing facilities easy to use. When seeing those, I always recall the matlab C++ code generator, and the code it generated was frankly rubbish (Your code is good, it's just that those aliases trigger the feeling of disgust).
Take by std::string_view
I know you tagged this as C++14, but support for C++17 language is already good on all compilers, and standard library implementations are catching up. std::string_view
is just more explicit way of saying that you don't need a copy of a string, just wanna read it.
Less verbose ways to overload
I'd be inclined to use if constexpr
or tag based dispatch. The latter would probably end up the same length as current code.
Don't inhibit RVO
auto s = std::move(format(str, std::forward<Ts>(args)...));
RVO/NRVO (named return value optimization) is an optimization performed by compiler which is a form of copy elision. Compilers are allowed to eliminate a copy in some cases (can't recall strictly in which cases) Move would happen anyway, and in C++17 the guaranteed copy elision would work anyway, too.
Easy to use correctly, hard to use incorrectly
Perhaps I read too much of "Nobody wants to read your sh*t", but I see some similarity between what Scott Meyers writes and what Steven Pressfield describes. Both try to convey idea of being user/reader-friendly. Guiding them at the correct usage/idea, rather than expecting them to know it (or at least to try one's best). At the moment, the code doesn't throw exception if number of arguments is not equal to number of replacement symbols in the format string, nor any other warning/error. Not everybody knows that the functions don't support custom types with operator<<
overloaded. C++ users usually use std::fstream
, or some other custom build streams, which the code doesn't support. Very few C++ programmers are paranoid about ADL, which the code will invoke in case the intended print didn't compile. Some ADL calls are extremely evil.
People praise code for being nice, and great interface and reasonable performance is sufficient definition of nice (IMO). Implementation can be amended as time goes by, but changing interface/contracts is rarely an option. Although implementation matters, having great interface offers much greater possibilities of optimizations.
Executable size is not that important
Size of iostream
gets dwarfed by the rest of application, usually. Using standard library algorithms, or especially boost, will make additional size of iostream
insignificant. It might be important in some cases, but those who encounter would usually have no std::FILE
either, as they want to output to embedded 8*8 LED screen or something like that.
Alternative implementation
I would use pseudo runtime tuple indexing to access elements, rather than recursing. That would eliminate the weird edge case of no arguments, and make everything more consistent.
Important excerpt:
template <std::size_t static_index, typename ... ArgumentTypes>
std::string runtime_get(std::size_t runtime_index, std::tuple<ArgumentTypes...>&& rest) {
if constexpr (static_index == std::size_t(-1)) {
throw std::invalid_argument("number of format symbols is more than arguments");
}
else {
if (runtime_index == static_index)
return to_string(std::get<static_index>(rest));
else
return to_string(runtime_get<static_index - 1>(runtime_index, std::move(rest)));
}
}
template <typename... Ts>
string_t format(const CharT *str, const Ts &... args) {
std::size_t current_index = 0;
string_t res;
for (; *str; str++) {
if (*str != '%') {
res += *str;
continue;
}
if (*(str + 1) == '%') {
res += '%';
++str;
}
else {
res += runtime_get<sizeof...(Ts) - 1>(current_index,
std::forward_as_tuple(args...));
++current_index;
}
}
return res;
}
It is easy to see that one can only pass data into a function, but trying to get the wanted element is impossible due to type being different depending on runtime index. The function usually compiles into a loop for differing user defined types, and into jump table in case of the same or only built in types.
Full code of alternative implementation:
#include <cstdlib>
#include <string>
#include <stdexcept>
#include <tuple>
namespace gupta {
using string_t = std::string;
using CharT = string_t::value_type;
template <typename T> inline string_t to_string(const T &f) { return std::to_string(f); }
inline string_t to_string(const char *str) { return std::string{ str }; }
inline string_t to_string(string_t s) { return (s); }
template <std::size_t static_index, typename ... ArgumentTypes>
std::string runtime_get(std::size_t runtime_index, std::tuple<ArgumentTypes...>&& rest) {
if constexpr (static_index == std::size_t(-1)) {
throw std::invalid_argument("number of format symbols is more than arguments");
}
else {
if (runtime_index == static_index)
return to_string(std::get<static_index>(rest));
else
return to_string(runtime_get<static_index - 1>(runtime_index, std::move(rest)));
}
}
template <typename... Ts>
string_t format(const CharT *str, const Ts &... args) {
std::size_t current_index = 0;
string_t res;
for (; *str; str++) {
if (*str != '%') {
res += *str;
continue;
}
if (*(str + 1) == '%') {
res += '%';
++str;
}
else {
res += runtime_get<sizeof...(Ts) - 1>(current_index,
std::forward_as_tuple(args...));
++current_index;
}
}
return res;
}
template <typename... Ts> auto fprint(std::FILE *f, const char *str, Ts &&... args) {
auto s = std::move(format(str, std::forward<Ts>(args)...));
return fwrite(s.data(), 1, s.size(), f);
}
template <typename... Ts> inline auto print(const char *str, Ts &&... args) {
return fprint(stdout, str, std::forward<Ts>(args)...);
}
template <typename... Ts> inline auto debug(const char *str, Ts &&... args) {
return fprint(stderr, str, std::forward<Ts>(args)...);
}
namespace detail {
class _stdout_object {};
class _stderr_object {};
} // namespace detail
namespace printing_shortcuts {
template <typename T> detail::_stdout_object operator<<(detail::_stdout_object f, const T &arg) {
auto s = to_string(arg);
fwrite(s.data(), 1, s.size(), stdout);
return f;
}
template <typename T> detail::_stderr_object operator<<(detail::_stderr_object f, const T &arg) {
auto s = to_string(arg);
fwrite(s.data(), 1, s.size(), stderr);
return f;
}
detail::_stdout_object print() { return {}; }
detail::_stderr_object debug() { return {}; }
} // namespace printing_shortcuts
using namespace printing_shortcuts;
} // namespace gupta
using namespace gupta::printing_shortcuts;
class test {};
std::string to_string(const test &) { return "test"; }
#include <assert.h>
#include <iostream>
int main() {
using namespace gupta;
std::cout << format("%% %", 12) << '\n';
assert(format("%% %", 12) == "% 12");
}
-
\$\begingroup\$ For those who wonder why I mentioned "Nobody wants to read your sh*t", I'm writing a book in literary fiction genre (the one about emotions and stuff, no scifi). \$\endgroup\$Incomputable– Incomputable2018年07月24日 15:10:06 +00:00Commented Jul 24, 2018 at 15:10
-
\$\begingroup\$ "Should increment twice and also replace two % with one %" - that's exactly what i want to do, double "%%" will print single '%'..... thanks for your advices \$\endgroup\$bluedragon– bluedragon2018年07月24日 15:10:28 +00:00Commented Jul 24, 2018 at 15:10
-
\$\begingroup\$ @bluedragon,
format("%%")
is also a big confusing. I would expect%
, but it returns the string itself. I deleted the part about bug, as after some more testing it turns out to be correct. It's just that format with no arguments got me off the tracks. \$\endgroup\$Incomputable– Incomputable2018年07月24日 15:12:57 +00:00Commented Jul 24, 2018 at 15:12 -
\$\begingroup\$ @bluedragon, I added a technique which you might be interested in. If you need C++14 version, just use
std::integral_constant
as argument. \$\endgroup\$Incomputable– Incomputable2018年07月24日 15:53:10 +00:00Commented Jul 24, 2018 at 15:53
I don't think there's a good reason for renaming std::string
like this:
using string_t = std::string;
It just serves to make the code harder to read (because I have to remember this alias). Just use std::string
as is. Similarly, use char
where appropriate (as it is, format()
and fprint()
aren't consistent in this respect.
This function:
template <typename T> inline string_t to_string(const T &f) { return std::to_string(f); }
catches all T
not otherwise overloaded. I'd be inclined to
using std::to_string;
That then leaves us free to write a catch-all template, something like
template <typename T>
std::string to_string(const T& val)
{
std::ostringstream s;
s << val;
return s.str();
}
The advantage to this is that many class authors will have written operator<<(std::ostream&, T);
already, which saves you bullying them to also write a to_string(T)
.
We could constrain this template (with Concepts, or with std::enable_if
, or with an anonymous template argument).
Exercise: Extend the code to work with other kinds of string (e.g. std::wstring
).
-
\$\begingroup\$ I don't like iostream, just including the header doubles my executable size on mingw \$\endgroup\$bluedragon– bluedragon2018年07月24日 14:28:00 +00:00Commented Jul 24, 2018 at 14:28
-
\$\begingroup\$ `using std::to_string;' can adl still be used \$\endgroup\$bluedragon– bluedragon2018年07月24日 14:29:00 +00:00Commented Jul 24, 2018 at 14:29
-
\$\begingroup\$ Re exercise: Wouldn't changing
string_t
tousing string_t = std::wstring;
accomplish that? ^^ \$\endgroup\$hoffmale– hoffmale2018年07月24日 14:47:37 +00:00Commented Jul 24, 2018 at 14:47 -
\$\begingroup\$ No, because then it would still only work for one kind of string. I meant that it should work with a
std::basic_string<T>
for anyT
. IOW, can the code be templated on the character type? \$\endgroup\$Toby Speight– Toby Speight2018年07月24日 14:49:48 +00:00Commented Jul 24, 2018 at 14:49 -
\$\begingroup\$ @TobySpeight, I'd put SFINAE on the ones that are common, and force the user to provide conversions for the rest (not hoffmale). \$\endgroup\$Incomputable– Incomputable2018年07月24日 14:53:46 +00:00Commented Jul 24, 2018 at 14:53
Explore related questions
See similar questions with these tags.
printf()
implementation. github.com/Loki-Astari/ThorsIOUtil/blob/master/src/ThorsIOUtil/… \$\endgroup\$