It's often convenient to use C-style printf
format strings when writing C++. I often find the modifiers much simpler to use than C++ I/O manipulators, and if I'm cribbing from existing C code, it helps to be able to re-use the existing format strings.
Here's my take on creating a new std::string
given a format and the corresponding arguments. I've given it an annotation so GCC can check the argument types agree when the format is a literal, but I don't know how to help other compilers.
I've provided a more memory-efficient version for C++17, which provides read/write access to the underlying array of a string. My copy of CPP Reference still says "Modifying the character array accessed through data()
has undefined behavior", but the Web version has been edited (May 2017) to indicate that it's only the const
version that has that constraint.
For earlier standards (I require minimum C++11), we may need to allocate a temporary array, as we can't write to a string's data. Unfortunately, this requires an extra allocation and copy.
#include <string>
std::string format(const char *fmt, ...)
#ifdef __GNUC__
__attribute__ ((format (printf, 1, 2)))
#endif
;
// Implementation
#include <cstdio>
#include <cstdarg>
#if __cplusplus < 201703L
#include <memory>
#endif
std::string format(const char *fmt, ...)
{
char buf[256];
va_list args;
va_start(args, fmt);
const auto r = std::vsnprintf(buf, sizeof buf, fmt, args);
va_end(args);
if (r < 0)
// conversion failed
return {};
const size_t len = r;
if (len < sizeof buf)
// we fit in the buffer
return { buf, len };
#if __cplusplus >= 201703L
// C++17: Create a string and write to its underlying array
std::string s(len, '\0');
va_start(args, fmt);
std::vsnprintf(s.data(), len+1, fmt, args);
va_end(args);
return s;
#else
// C++11 or C++14: We need to allocate scratch memory
auto vbuf = std::unique_ptr<char[]>(new char[len+1]);
va_start(args, fmt);
std::vsnprintf(vbuf.get(), len+1, fmt, args);
va_end(args);
return { vbuf.get(), len };
#endif
}
// Test program
#include <iostream>
int main()
{
std::clog << "'" << format("a")
<< "'" << std::endl;
std::clog << "'" << format("%#x", 1337)
<< "'" << std::endl;
std::clog << "'" << format("--%c--", 0) // an embedded NUL
<< "'" << std::endl;
std::clog << "'" << format("%300s++%6.2f", "**", 0.0).substr(300)
<< "'" << std::endl;
}
void provoke_warnings()
{
// warning: zero-length gnu_printf format string
// [-Wformat-zero-length]
std::clog << "'" << format("") << "'" << std::endl;
// warning: format ‘%c’ expects argument of type ‘int’, but
// argument 2 has type ‘const char*’ [-Wformat=]
std::clog << "'" << format("%c", "bar") << "'" << std::endl;
}
I've compiled the code with both C++17 and C++11 compilers, and verified them both under Valgrind using this test program.
I'd welcome any comments on the code itself or on my testing.
return
statement. But I have to admit I'm not even consistent these days... \$\endgroup\$256
and offer advice for a better number and a name for it. \$\endgroup\$reluctant to tell
anybody to use braces even for single line statements. Though the bugs caused by this are rare nowadays (because of reduced macro usage) they are now doubly hard to spot. So chance of an error is small, but when you get an error it is extremely hard to spot. Using braces avoids the possibility of an error caused by multi line macros. So my advice is always use braces. \$\endgroup\$