I like this functionality a lot, it has been sorely lacking in C++ and is becoming more and more a standard in many other languages.
Some comments:
- I would like the part before the : inside {} to allow any text. This allows translators to understand better the meaning of the inserted value and thus produce better translations. Not writing anything before the : would still
be equivalent to writing the next consecutive number. I think you removed this capability from the python implementation as C++ does not have named arguments but it still has value in conveying information between programmer and the person doing translation.
- Some kind of standard interface to internationalization string replacement could be incorporated in format(), so that we don't have to write _() or something around the literal.
- How to specify using . for decimal char in a country where , is standard and vice versa. n only seems to relate to thousand separator handling, but the usual problem is to get the right decimal character in float number output. Many file formats (JSON for instance) require a dot even in countries where comma is standard. This is a recurring problem as "someone else" may set your locale on a global level. I have a similar library where I use comma or dot (in the dot position in the format string) to denote themselves as decimal char while semicolon denotes "the locale decimal char". While creating your own buffer type which overrides the locale() method is possible it is much more work than defining the decimal char in the format string.
- I don't particularly like the partial reuse of the C formatting conventions with a specified order of the parts of the format string, as it is hard to learn. Sure, it is rather easy whern you have learned printf, but do we want future generations of programmers have to go through that detour? Better start from scratch with something logical, where the order of characters is not crucial.
- It may be better to send the formatting string snippet to format_value() as a basic_string_view rather than relying on all the function overloads to remember to bump the ptr correctly. At least provide a method in ctx to forward the ptr and return the basic_string_view containing the format part in case you want to preserve the possibility to nest {} inside the formatting string. This simplifies for the fairly large share of format_value functions which are implemented but don't care about any formatting details.
- A maximum inserted length is often useful to limit the output. One case is when formatting large doubles in the f format. This can get ridiculous in printf. Another is potentially long file names, where you may want to limit the string length.
- It is hard to understand the "arg store" idea and how this goes together with calling format_value on each parameter. It may be that the fmt::args and basic_args are actually the same. There is also arg_store and basic_arg and the visit() function which seem more like internal details of the implementation. Even as an implementation it seems overly complex, and I for one can't understand how visit can call separate user defined format_value functions if arg_store is not templated on the argument types.
- It would be nice if it was easy to create a formatting object from a format specification string. This object should have methods to format the standard types that fomat() works for "out of the box". In this way you can easily override format_value for instance for std::vector<T>, passing each element to a formatting object you have created. Of course you can also call format_value() for each T provided it exists, but this incurs quite an overhead as the same format specification string is parsed over and over again for each array element. Taking this thought to the logical conclusion means that the customization point for a user defined type should be a function create_formatter<T>(string_view format_specification_string) rather than format_value. The formatter returned from create_formatter then has a method format(const T& value) which does the formatting job. This allows the vector optimization to be taken to vectors of vectors etc. Continuing on this tangent I think it may make sense to allow pre-creating a formatting object for an entire format string, so that the parsing of that string occurs only once. Lets call this class formatter<Ts...>. Example:
template<typename... Ts> class formatter {
public:
formatter(string_view format_string); // This parses the format_string and stores the objects returned from create_formatter<T> for each of the Ts.
void run(buffer& buf, const Ts&.. args); // Perform the formatting to a buffer
string run(buffer& buf, const Ts&... args); // Perform the formatting and return a string
};
// The format function is then defined as:
template<typename... Ts> string format(string_view format_string, const Ts& values)
{
formatter<Ts...> fmt(format_string);
return fmt.run(values...);
}
// A good compiler hopefully generates the same code as in the current implementation.
// A loop for printing many lines would be more effective if written:
formatter<int, string, double> fmt("#{IX}: {NAME}, {PRICE:.2"); // Preparse format
for (item : inventory)
cout << fmt.run(item.ix,
item.name, item.price);