9
\$\begingroup\$

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.

\$\endgroup\$
1
  • \$\begingroup\$ I made some changes that hopefully makes the code easier to read. I refactored 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\$ Aug 20, 2013 at 1:46

2 Answers 2

3
\$\begingroup\$

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.

\$\endgroup\$
2
  • \$\begingroup\$ I didn't know about std::to_string overloading! I did it primarily to support any class that overloads operator<<. Thank you for the feedback! \$\endgroup\$ Dec 10, 2013 at 4:13
  • \$\begingroup\$ I believe it would be possible to just call to_string(val), and provide a template ext::to_string() that uses <<, for those classes without their own to_string(). \$\endgroup\$ Jan 17, 2020 at 9:07
0
\$\begingroup\$

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.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.