-
Notifications
You must be signed in to change notification settings - Fork 2.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[WIP] Implementation of fmt::prepare. Implements #618 (and probably #613) #949
Conversation
I'll handle failed builds ASAP + will run benchmarks to see how my implementation looks among other ways of formatting. |
Wow, that's an impressive PR, thank you for working on it! I'll take a look in details but first a few quick questions regarding the design:
Could you elaborate how statefulness relates to allocator-awareness? Is it because you use a container (string) to store some of the state?
How does it work with
Why and how does it work with existing parsers that report errors via exceptions? |
Exactly. Prepared format has two members that can allocate memory - a format and a parts container. We need to provide a way to specify allocators that are used to allocate memory for these members.
Sorry, I forgot to mention the constexpr case. When a format's type is the fmt's compile time string, the stored member is an instance of the struct that wraps the string literal (the struct, returned from the FMT_STRING's lambda), otherwise it's a
Well, while writing an answer to this, I realized that I can take advantage of a parse_context to check whether a specification was successfully parsed by a custom formatter. Thanks to that, the change in the public API won't be needed. I'll do the changes and push a commit probably tomorrow. |
I'll squash commits just before the merge. It'd be more convenient for me to keep them separated while working on this pr. |
A user format string should be convertible to
Great! So the "Current API breaking changes" section is no longer relevant?
That seems OK, but I it would be nice to have a shorter name to make tests more readable. Maybe just
Yes, please. Runtime format strings are the primary API and should be tested at least as much as the compile-time ones. |
Didn't know that user format string needs to be a variation of a
Sure, done.
Will do. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A few more comments.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I will have another pass over the changes after you address the current comments. In general, I think this is a good approach.
2247513
to
dc2a2ae
Compare
I hope that I removed all whitespaces-only changes, sorry again for that. Also, sorry for taking so long to reply/make changes - I had tough week with a cold. I know that I didn't reply for one comment, I won't be able to do it today because this topic needs a deeper discussion and I want to prepare my point of view. I'll try to do it tomorrow. Now I wanted to address as much as I could today. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry for a slow reply, it's a busy time of year. I thought about this more and here are some things that IMHO need to be done:
- Remove prepared specs from
basic_format_context
. This will break dynamic formatter but it doesn't matter because custom formatting requires more work anyway. I think we should focus on core functionality here and add support for custom (including dynamic) formatters later. - Merge
prepared_format_specs
intodynamic_specs
(makedynamic_specs
relocatable). This should simplify things a bit.
Implementation of fmt::prepare() function and features around it.
I squashed the commits and made changes as agreed:
I'm aware that android build fails (not sure why gradle is not able to find Ninja). I'll handle this asap. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the update, I think we are getting close.
include/fmt/core.h
Outdated
@@ -877,16 +881,18 @@ FMT_CONSTEXPR typename internal::result_of<Visitor(int)>::type | |||
template <typename Char, typename ErrorHandler = internal::error_handler> | |||
class basic_parse_context : private ErrorHandler { | |||
private: | |||
basic_string_view<Char> format_str_; | |||
int next_arg_id_; | |||
basic_string_view<Char> primary_format_str_; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is primary_format_str_
needed? Shouldn't format_str_
be enough to resolve references?
Also the indent is a bit off.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
AFAIU, while parsing/formatting, basic_parse_context::advance_to()
can be called, which calls format_str_.remove_prefix()
, which 'invalidates' format_str_
and arg_ref::name
(which is string_view_metadata
) can not be created because it needs primary format string to calculate offset from the beginning of the format. Does that make sense, or I just messed up something?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right now it is not a problem because we agreed to address custom formatting separately.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So basic_parse_context::advance_to()
is called only for custom formatters?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
More or less. The whole context machinery is there to support extension mechanism (custom formatters).
include/fmt/format.h
Outdated
@@ -1585,68 +1596,79 @@ class specs_setter { | |||
basic_format_specs<Char> &specs_; | |||
}; | |||
|
|||
template <typename ErrorHandler> class numeric_specs_checker { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This seems no longer needed and can be integrated back into specs_checker
(just revert specs_checker
changes).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it's still useful because it's used in two places: dynamic_specs_handler
and prepared_format
while formatting, to check already prepared specs.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, I missed the second use.
I merged the main part of this PR with some tweaks. The part that is not merged is test refactoring as I'm not sure that it's a good idea to type parameterize the tests. Will explain in more details a bit later. Thanks for working on this! |
To elaborate on my comment about the tests, I think it would be nice to avoid type parametrization because
|
I am glad that the main part has been merged! Thanks for the time that you put into review. Good points about the tests. I'll take a closer look on them, figure out what really needs to be tested and provide a pull request or create an issue to start a discussion. |
@stryku, could you please review the updated CONTIBUTING document, particularly the part about licensing, and let me know if you agree with it being applied to your contributions to {fmt}? The library is likely to be relicensed (#1073) so I'm collecting approval from all earlier contributors. Thanks! |
Foreword
Hello (: First of, this thing is huge. Please, take your time while reviewing it. I'm open for a discussion and aware that this overview doesn't explain everything, so don't hesitate to ask questions, I'll try to explain why I did this in such way, that in other way etc.
Abstract
This pull request's purpose is to implement a
fmt::prepare<>()
function and all functionality around it.Problems
TL;DR solutions
FMT_USE_CONSTEXPR
is enabled andfmt::prepare
detects that passed formatis_compile_string
, then format will be prepared at compile time.fmt::prepared_format<>
typedef/alias.std::basic_string<>
."My name is {0}."
format, there are three parts: text"My name is "
, argument id0
, text"."
). User is able to provide his own types for the format and prepared parts container via afmt::basic_prepared_format<>
(I'll talk about it later).API
New API
format_part<> class
A class that represents a format prepared part. It's not internal, to give user possibility to pass his own parts container type (see below).
parts_container<> typedef
A typedef of a class that is used as a parts container by prepared format. User can provide his own underlying container type (in, of course, the second parameter).
basic_prepared_format<> typedef
A typedef of a class that represents a fully customizable prepared format. User can provide his own Format and PartsContainer types.
prepared_format<> typedef
A simple alias for basic_prepared_format with default Format and PartsContainer types.
wprepared_format<> typedef
Same as above, but for wide strings.
prepare<>()
A function that returns a prepared format object.
Args
are the argument types that prepared format accepts in format functions.Prepared formats created by all of the above ways, provide the same functionality (same as plain fmt::format*() functions):
-format()
-format_to()
-format_to_n()
-formatted_size()
Tests
There was a need to a little refactor them. I have had to change tests strictly related to formatting, to be able to test multiple ways of formatting. Those tests are moved to a new file:
common-format-test.cc
, and they test the oldfmt::format()
and two versions offmt::prepare()
: one with runtime format string passed and one with compiletime format string.In
common-format-test
I've used gtest's TYPED_TESTs. There are three wrappers:-FormatFunctionWrapper: simply call
fmt::format*()
.-RuntimePreparedFormatWrapper: explicitly convert passed format string to a runtime format (which is std::basic_string, with proper character type). Then, create a prepared format using the shiny new fmt::prepare(). The last step is to format string using prepared format's
format()
method.-CompiletimePreparedFormatWrapper: this one is enabled only if FMT_USE_CONSTEXPR is enabled. It prepares format (using compiletime format string) and then, formats stuff using prepared format's
format()
method.As you can see, in almost every test, format string is wrapped by the
TEST_FMT_STRING
FMT
. That's because I couldn't think of a better, generic way to pass compiletime format string to wrappers. I wanted to do this, to testsfmt::prepare()
with compiletime format string.[DONE]
(Now I realized, that I probably should create one more wrapper:RuntimeFormatFunctionWrapper
(and renameFormatFunctionWrapper
toCompiletimeFormatFunctionWrapper
) that would explicitly convert a compiletime string to a runtime one and testfmt::format()
against it. What do you think?)Documentation
Didn't write it, simply because a lot can be changed during the review. I'll provide it as a last step of this pull request.
Hacks (or not)
basic_format_context::prepared_specs*
member anddynamic_formatter
.basic_format_context
has a newprepared_specs*
member. It is set, only ininternal::prepared_format::vformat_to()
method. Then, it's used bydynamic_formatter
to check whether it needs to parse specification, or it is already parsed and ready to use.Actually, this workaround is needed for only one of the tests, the one with variant:
It fails with old approach because:
-when using
fmt::format
, automatic argument id is tracked by a parse context. Later on, dynamic formatter gets that id from the parsed context.-when using
fmt::prepare
, all argument ids are tracked by the prepared format. Specification is parsed and prepared, long before actual formatting. Parse context doesn't track the id anymore, so when dynamic formatter was asking for it, parse context returned a corrupted id. Now, this prepared specification is handled by prepared format and set inbasic_format_context::prepared_specs*
. If dynamic_formatter detects that it can use an already prepared specs, it doesn't bother the parse context.An alternative would be to create a new context e.g.
prepared_format_context
, that would have this member. Then indynamic_formatter
, we would overloadformat
method to handle the new context. I decided to not do that (at least in scope of this pull request) because it would require a refactor, to pass format context template parameter in a couple (a lot?) of places. This pull request is already big, even without this change.Unrestricted unions
These are the unions with e.g. non-POD types. I needed them in
format_part
and changedarg_ref
.The simplest solution when they are not supported was to simply use a structure instead. I agree that it is not the best solution but unrestricted unions are supported since GCC 4.6, Clang 3.3 and Visual Studio 15 (MSVS >= 1900), so we support a couple of really old compilers that doesn't support that.
An alternative would be to use PODs in all unions and implement separate class-wrappers, that would implement needed methods handling those PODs. This would introduce a fine amount of new code (e.g. I'd need to introduce a new POD type for parsed_format_specs + a wrapper with methods for it, and more..). That's why I went this way.
Other
basic_prepared_format<>
This typedef gives user a possibility to provide his own types for held stuff. Thanks to that, we're fully allocator-aware. User can provide:
-A format string type.
-A parts container type.
Parts container needs to fulfill the PartsContainer concept, which is:
Concept is checked at a compile-time by the
parts_container_concept_check
struct.parts_container_concept_check (prepare.h)
There is a lot of boilerplate. It could be done with a macro, but I decided that macros for such stuff are yuck.
[DONE]
(I just realized that I probably should add checks for default, move and copy ctors there. What do you think?)prepare.h
I decided to create a new file for features related to
fmt::prepare()
. Thanks to that, user can avoid compilation of heavy templates when he doesn't need thefmt::prepare()
.