We need to format strings all the time. It would be so nice to be able to say:
std::string formattedStr = format("%s_%06d.dat", "myfile", 18); // myfile_000018.dat
Is there a C++ way of doing this? Some alternatives I considered:
snprintf
: uses raw char
buffers. Not nice in modern C++ code.std::stringstream
: does not support format pattern strings, instead you must push clumsy iomanip objects into the stream.boost::format
: uses an ad-hoc operator overload of %
to specify the arguments. Ugly.Isn't there a better way with variadic templates now that we have C++11?
It can certainly be written in C++11 with variadic templates. It's best to wrap something that already exists than to try to write the whole thing yourself. If you are already using Boost, it's quite simple to wrap boost::format
like this:
#include <boost/format.hpp>
#include <string>
namespace details
{
boost::format& formatImpl(boost::format& f)
{
return f;
}
template <typename Head, typename... Tail>
boost::format& formatImpl(
boost::format& f,
Head const& head,
Tail&&... tail)
{
return formatImpl(f % head, std::forward<Tail>(tail)...);
}
}
template <typename... Args>
std::string format(
std::string formatString,
Args&&... args)
{
boost::format f(std::move(formatString));
return details::formatImpl(f, std::forward<Args>(args)...).str();
}
You can use this the way you wanted:
std::string formattedStr = format("%s_%06d.dat", "myfile", 18); // myfile_000018.dat
If you don't want to use Boost (but you really should) then you can also wrap snprintf
. It is a bit more involved and error-prone, since we need to manage char buffers and the old style non-type-safe variable length argument list. It gets a bit cleaner by using unique_ptr
's:
#include <cstdio> // snprintf
#include <string>
#include <stdexcept> // runtime_error
#include <memory> // unique_ptr
namespace details
{
template <typename... Args>
std::unique_ptr<char[]> formatImplS(
size_t bufSizeGuess,
char const* formatCStr,
Args&&... args)
{
std::unique_ptr<char[]> buf(new char[bufSizeGuess]);
size_t expandedStrLen = std::snprintf(buf.get(), bufSizeGuess, formatCStr, args...);
if (expandedStrLen >= 0 && expandedStrLen < bufSizeGuess)
{
return buf;
} else if (expandedStrLen >= 0
&& expandedStrLen < std::numeric_limits<size_t>::max())
{
// buffer was too small, redo with the correct size
return formatImplS(expandedStrLen+1, formatCStr, std::forward<Args>(args)...);
} else {
throw std::runtime_error("snprintf failed with return value: "+std::to_string(expandedStrLen));
}
}
char const* ifStringThenConvertToCharBuf(std::string const& cpp)
{
return cpp.c_str();
}
template <typename T>
T ifStringThenConvertToCharBuf(T const& t)
{
return t;
}
}
template <typename... Args>
std::string formatS(std::string const& formatString, Args&&... args)
{
// unique_ptr<char[]> calls delete[] on destruction
std::unique_ptr<char[]> chars = details::formatImplS(4096, formatString.c_str(),
details::ifStringThenConvertToCharBuf(args)...);
// string constructor copies the data
return std::string(chars.get());
}
There are some differences between snprintf
and boost::format
in terms of format specification but your example works with both.
The fmt library implements exactly that, string formatting using variadic templates. Example:
// printf syntax:
std::string formattedStr = fmt::sprintf("%s_%06d.dat", "myfile", 18);
// Python-like syntax:
std::string formattedStr = fmt::format("{}_{:06}.dat", "myfile", 18);
Disclaimer: I'm the author of the library.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With