cout << format(
"%? %?!\n" // Hello world!
"%1% %3% :)\n" // Hello again :)
"%%05X: %05X\n" // %05X: 01234
"%%g: %g\n" // %g: 3.14159
"%%.3f: %5$.3f\n" // %.3f: 3.142
, "Hello", "world", "again"
, 0x1234, 3.14159265f);
- The above shows the usage of universal formatting -
%?
which wil just take the argument and pass it toostream << it
("Hello"
and"world!"
). - Next line shows positional specifier -
%1% %3%
(alternatively%1$? %3$s
) to explicitly select first and third arguments ("Hello"
and"again"
). %%
outputs single%
,0
changes to zero-padding,5
sets the width andX
will do:
out << hex << uppercase << it
.%g
isout << defaultfloat << it
and probably still needs some work.%5$.3f
uses positional and precision specifiers.
How it works
The format
is variadic function that will create format_pack
which is derived from my data pack
:
/// Tag for data pack with format string
struct format_pack_tag {};
/// Data pack with format string
template<class String = string, class... Elements>
class format_pack
: public format_pack_tag
, public pack<String, Elements...> {
public:
template<class... Args>
format_pack(Args&&... args)
: pack<String, Elements...>
( forward<Args>(args)...) {}
};
/// Create data pack with format string
template<class String, class... Elements>
inline format_pack<remove_cvref_t<String>,
remove_cvref_t<Elements>...>
format(String&& fmt, Elements&&... e) {
return format_pack<remove_cvref_t<String>,
remove_cvref_t<Elements>...>(
forward<String>(fmt),
forward<Elements>(e)...);
}
//=======================================================================
/// Stream-out the string
template<class String>
inline ostream& operator << (ostream& out,
const format_pack<String>& it) {
return out << it.value;
}
/// Stream-out the formatted data
template<class String, class... Elements>
inline ostream& operator << (ostream& out,
const format_pack<String, Elements...>& it) {
formatting::formatter(out, c_str(it.value), it.next);
return out;
}
There are some helpers used:
remove_cvref_t
=remove_cv_t<remove_reference_t<T>>
(C++14 style)c_str
will either return itsconst char *
argument or callc_str()
onstd::string
.
The Formatter
/// Formatting engine (short-lived, designed to do the job and dismiss)
struct formatter {
const char *fmt;
ostream& out;
state sta;
///\copydoc formatting::advance()
const char * advance() {
return fmt = formatting::advance(fmt, out);
}
///\copydoc formatting::prepare()
const char * prepare() {
const char * pos = formatting::prepare(fmt, out, sta);
if (!pos) {
out.clear();
out << (fmt-1);
out.setstate(ios::failbit);
}
return fmt = pos;
}
///\copydoc formatting::output()
template<class T>
const char * output(const T& data) {
const char * pos = formatting::output(fmt, out, data, sta.flags);
if (!pos) {
out.clear();
out << fmt;
out.setstate(ios::failbit);
}
return fmt = pos;
}
///\copydoc output()
template<class T>
void operator() (const T& data) {
output(data);
}
/// Stream-out the format string (no data)
formatter(ostream& out, const char * fmt)
: out(out), fmt(nullptr) {
out << fmt;
}
/// Stream-out formatted data
template<class... Elements>
formatter(ostream& out, const char * fmt
, const pack<Elements...>& data)
: out(out), fmt(fmt) {
sta.apos = 0;
for(;;) {
if (!advance()) return;
if (!prepare()) return;
if (!data.exec(sta.apos, *this)) break;
++sta.apos;
}
out.setstate(ios::failbit);
out << fmt;
fmt = nullptr;
}
};
The Formatting (the hidden logic)
advance()
will find the%
(skipping any%%
)prepare()
will parse positional, width and precision specifiersoutput()
will output the value and is overloaded with some SFINAE
(arithmetic, pointers, strings and all other).
namespace formatting {
///\brief Advance to next format specifier (\c '%')
///\return position of '%' or nullptr
inline const char* advance
( const char* pos ///< position in format string
, ostream& out ///< output stream
) {
const char* at = pos;
for(;;) {
if (*at != '%') do {
if (!*at) {
out.write(pos, at-pos);
return nullptr;
}
} while (*++at != '%');
if (*++at != '%') break;
out.write(pos, at-pos);
pos = ++at;
}
out.write(pos, at-1-pos);
return at;
}
/// Formatter state
struct state {
public:
uint apos; ///< argument position
uint flags; ///< additional flags
enum {
space = 1<< 0, ///< ' ' specifier (sign)
center = 1<< 1 ///< '=' specifier (alignment)
};
};
//TODO: use flags in output()
//=======================================================================
///\brief Prepare output stream state
///\return position of conversion specifier (after \c %w.p)
/// or `nullptr` on error + failbit on `out` (can throw).
inline const char *prepare
( const char* spec ///< position in format string after \c '%'
, ostream& out ///< output stream
, state& sta ///< formatter state
) {
out.flags(ios::right|ios::dec);
out.fill(' ');
out.width(0);
out.precision(6);
sta.flags = 0;
for(;;) {
char c = *spec++;
switch (c) {
// unknown character -> pass to output()
default:
return --spec;
// these are ignored (argument types for printf)
case 'h': case 'l': case 'j': case 'z': case 't': case 'L':
continue;
// adjustfield
case '-':
out.setf(ios::left, ios::adjustfield);
continue;
case '=':
sta.flags |= state::center;
continue;
// fill
case '0':
out.fill('0');
continue;
// sign
case '+':
out.setf(ios::showpos);
continue;
case ' ':
sta.flags |= state::space;
continue;
// width or position
case '1': case '2': case '3': case '4':
case '5': case '6': case '7': case '8':
case '9':
{ const char* num = spec-1;
while (isdigit(*spec)) ++spec;
int w = atoi(num);
if (*spec == '%' || *spec == '$') {
sta.apos = (uint)(w-1);
if (*spec == '%') {
return spec;
}
++spec;
continue;
}
out.width(w);
}
continue;
// precision
case '.':
//TODO: use some flag for this check
// if out.precision() != 6
// // second precision specifier
// out.setstate(ios::failbit)
// return nullptr
{ const char* num = spec;
while (isdigit(*spec)) ++spec;
out.precision(atoi(num));
}
continue;
}
}
}
//################################################################ output
#ifdef FIRDA_DOXYGEN_INVOKED_
///\brief Format the value.
///\return position after format specification
/// or `nullptr` on error + failbit on `out` (can throw).
template<class T> const char* output
( const char* spec ///< position in format string after '%'
, ostream& out ///< output stream
, const T& it ///< value to be formatted
, uint flags = 0 ///< formatter flags
);
#else
/// Format arithmetic value
template<class T> inline
enable_if_t<is_arithmetic<T>::value,
const char*>
output
( const char* spec ///< position in format string after '%'
, ostream& out ///< output stream
, T it ///< value to be formatted
, uint flags = 0 ///< formatter flags
) {
char c = *spec++;
if (isupper(c)) {
c = tolower(c);
out.setf(ios::uppercase);
}
switch (c) {
default:
out.setstate(ios::failbit);
return nullptr;
case '?': case '%':
out << it;
break;
case 'd': case 'i':
out << dec << try_make_signed_t<T>(it);
break;
case 'u':
out << dec << try_make_unsigned_t<T>(it);
break;
case 'x':
out << hex << it;
break;
case 'o':
out << oct << it;
break;
case 'g':
out << defaultfloat << it;
break;
case 'f':
out << fixed << it;
break;
case 'e':
out << scientific << it;
break;
//FIXME: consider signed vs. unsigned
case 'c': case 's':
out << char(it);
break;
}
//TODO: case '#' - special formatting
return spec;
}
//=======================================================================
/// Format pointer
template<class T> inline
enable_if_t<!is_same<T,char>::value,
const char*>
output
( const char* spec ///< position in format string after '%'
, ostream& out ///< output stream
, const T* it ///< value to be formatted
, uint flags = 0 ///< formatter flags
) {
char c = *spec++;
if (isupper(c)) {
c = tolower(c);
out.setf(ios::uppercase);
}
switch (c) {
case 'p':
out << hex << uintptr_t(it);
return spec;
default:
if (!it) {
//TODO: skip custom format
out << "null";
return spec;
}
return output(spec, out, *it);
}
}
//=======================================================================
/// Format string
template<class String> inline
enable_if_t<is_same<String,const char *>::value
|| is_same<String,const string&>::value,
const char*>
output
( const char* spec ///< position in format string after '%'
, ostream& out ///< output stream
, String it ///< string to be formatted
, uint flags = 0 ///< formatter flags
) {
out << it;
return ++spec;
}
//=======================================================================
/// Format generic
template<class T> inline
enable_if_t<!is_fundamental<decay_t<T>>::value
&& !is_pointer<decay_t<T>>::value,
const char*>
output
( const char* spec ///< position in format string after '%'
, ostream& out ///< output stream
, const T& it ///< value to be formatted
, uint flags = 0 ///< formatter flags
) {
char c = *spec++;
if (isupper(c)) {
c = tolower(c);
out.setf(ios::uppercase);
}
switch (c) {
//TODO: some conversions for cdiuxgfe (char, long long, long double)
default:
out.setstate(ios::failbit);
return nullptr;
case 's': case '?': case '%':
out << it;
break;
}
return spec;
}
#endif
Finally - delegated formatting
All I wanted was to create some engine for remote debug/trace log, to store all the data (with some code/ID) in some queue without actually formatting it and let another (debug) thread do the formatting (and output to cout
or syslog
). Just an example how it can be done:
struct record_base
virtual ostream& print(ostream&) = 0
virtual ~record_base() {}
template<class Pack>
struct record_impl
: record_base
Pack pack
template<class... Args>
record_impl(Args&&... args)
: pack(forward<Args>(args)...) {}
ostream& print(ostream& out) override
return out << pack
template<class String, class... Args>
unique_ptr<record_base>
record(String&& fmt, Args&&... args)
return unique_ptr<record_base>(new record_impl<
format_pack<string, remove_cref_t<Args>...> >
( forward<String>(fmt), forward<Args>(args)...) )
int main()
record(
"%? %?!\n" // Hello world!
"%1% %3% :)\n" // Hello again :)
"%%05X: %05X\n" // %05X: 01234
"%%g: %g\n" // %g: 3.14159
"%%.3f: %5$.3f\n" // %.3f: 3.142
, "Hello", "world", "again"
, 0x1234, 3.14159265f
).get()->print(cout)
prepare()
. Knows whatostream
knows). 5. It does not use temporary string, the output is direct. 6. I already had that data pack class and rqueue (because of PLCs), it was just natural to write thisformat()
. \$\endgroup\$advance
doesn't parse embedded strings diligently: in"\"%\""
it will pick up a percent sign as a formatting one. \$\endgroup\$printf
? I don't think so, you have to write double:"\"%%\""
, it is not designed to understand whatever logic, you may wish to format something within the string:"\"%s\""
\$\endgroup\$sprintf()
is not type safe. You should never use it in C++ code. This code should be type safe (not read the details). Butmonstrosity
is a very loaded word. If you look atstd::vector<>
under the covers its amonstrosity
but the interface it provides to the user should make using it clean and reduce errors. \$\endgroup\$