-
Notifications
You must be signed in to change notification settings - Fork 2.5k
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
Feature discussion: formatting at compile-time #1895
Comments
Floating-point formatting doesn't allocate in the common case.
I don't think it will be useful for everyone, but it will probably be useful for someone =). I can think of some use cases, e.g. compile-time error reporting.
FP is the most difficult case but pointers and user-defined types will also be problematic. |
supposed to be anyone, not everyone Anyway, I will continue my efforts as far as I can. Right now I don't bother to use any checks whatsoever: And yeah, this would be definitely a C++20 feature, because of |
Small update about remaining non-implemented compile-time formatting features. PointersI was hoping for C++20 constexpr
As far as I know, there are no other options to bit-cast anything at compile-time, so it seems no pointer formatting at compile-time. But I doubt that it's a handy feature. Floating-pointThere were several problems when I started:
For the latter problem I can propose 3 solutions:
This is why I don't really know what to do with this task right now. Also, I have some thoughts about the current FP formatting implementation in {fmt}, the Grisu one, that is used with almost all specs. I took a quick look at this implementation and I cannot find a reason to format a significant part to separate buffer, considering that a bit later this separate buffer will be copied to the final output buffer with some minor changes. Of course, maybe I just overlooked something. |
Regarding constexpr FP formatting, I think the most feasible approach is to use Dragon4 for everything: Lines 2305 to 2306 in 308510e
It will mostly require making
We need to allocate the exact amount in the output which is not possible until after we generated digits. It also doesn't matter because the digits are moved around anyway. |
Hm... looks like I got this function wrong, because for some reason I assumed that
Sad to say, I didn't research the FP formatting completely, mostly because it seems non-trivial, this is probably why I have these thoughts. Anyway, I just thought maybe digits could be generated on-demand, if there is no dependency between digits, of course. In that case, there would be only one pass instead of two right now: generation of digits and applying format for these digits. Probably I'm not the first person who has this cool optimization idea, but besides that, I'm ignorant of this topic, that's why I don't see a reason to have 2 passes. |
Yes, it's Dragon4 with minor tweaks.
Fixed, thanks.
The problem is that we need to know the number of digits which for the shortest representation is almost as hard to compute as generating the digits themselves. It could be possible to do something for fixed precision representations but I'm not sure it's worth the increased complexity. |
I'm writing down here my observations about constexpr FP formatting. I started work on this a long time ago in my fork, but I stuck with some problems around the same time. Since I didn't come up with some nifty solutions for these problems yet - I'm sharing these problems here, otherwise, my work would be probably wasted.
|
Thanks for writing this down. I think it's OK to initially restrict the feature to the header-only mode. We are moving towards modules anyway so the distinction will eventually become irrelevant. The reason why the sign check doesn't work for
So the sign is bit 79, not 127. |
I keep experimenting with constexprifying {fmt}, and here is another interesting observation. As I mentioned before, theoretically, we can format string at compile-time not once (using the same technique already used in template <typename Integer> std::string format(Integer value) {
constexpr auto precompiled_format = test_format<8>(
FMT_COMPILE("{{:0{}b}}"), std::numeric_limits<Integer>::digits);
fmt::format(precompiled_format, value); // doesn't compile,
// but even with some fixes it uses
// runtime API, not compile-time
} In this particular case, a simple
Here is how the code can look for precompiled formats: constexpr auto compiled_format0 = FMT_COMPILE("{{:0{}b}}");
// holds "{{:0{}b}}", nothing interesting here
constexpr auto compiled_format1 = FMT_COMPILE_FORMAT(7, compiled_format0, 8);
// holds "{:08b}", and it can be passed to fmt::format() directly (compile-time API is used for it)
constexpr auto compiled_format2 = FMT_COMPILE_FORMAT(9, compiled_format1, 3);
// holds "00000011", there is no much sense to pass it to fmt::format(), but it can be passed 😏 where # define FMT_COMPILE_FORMAT(buffer_size, format, ...) \
[&]() { \
struct static_compiled_format : fmt::detail::compiled_string { \
using underlying_type = decltype(format); \
using char_type = underlying_type::char_type; \
\
std::array<char_type, buffer_size> buffer; \
\
constexpr static_compiled_format() \
: buffer([]() { \
auto temp_buffer = decltype(buffer){}; \
fmt::format_to(temp_buffer.data(), underlying_type{}, \
__VA_ARGS__); \
return temp_buffer; \
}()) {} \
\
constexpr explicit operator fmt::basic_string_view<char_type>() \
const { \
return {buffer.data(), buffer.size()}; \
} \
\
[[nodiscard]] constexpr static_compiled_format to_str() const { \
return {}; \
} \
\
[[nodiscard]] constexpr size_t size() const { return buffer.size(); } \
\
constexpr auto operator[](size_t pos) const -> const char_type& { \
return buffer[pos]; \
} \
}; \
return static_compiled_format{}; \
}() As you can see, there are some new methods for handling this class inside compile-time API, since we can work at compile-time with string literals easily, but not with static char buffers 🤦. The most important method here is [[nodiscard]] constexpr auto to_str() const {
return fmt::basic_string_view<char_type>(*this);
} AFAICS there are no more straightforward examples for this functionality. There is how it looks right now, it has some workarounds, but they can be fixed later. Anyway, I don't know if it's even worth adding, or maybe it can be implemented in a much better way. |
Interesting idea. Does |
Sorry, I didn't have time to check it out and answer earlier.
This idea can actually be implemented using templates instead of macros, thanks to non-type template parameters. In this case usages look something like: constexpr auto compiled_format0 = FMT_COMPILE("{{:0{}b}}");
constexpr auto compiled_format_1 =
templaty_compiled_format<7, compiled_format0, 8>{};
constexpr auto compiled_format_2 =
templaty_compiled_format<9, compiled_format_templaty_1, 3>{}; where template <size_t buffer_size, auto format_string, auto... args>
struct templaty_compiled_format : fmt::detail::compiled_string {
using format_string_type = decltype(format_string);
using char_type = format_string_type::char_type;
static constexpr auto buffer = []() {
auto temp_buffer = std::array<char_type, buffer_size>{};
fmt::format_to(temp_buffer.data(), format_string_type{}, args...);
return temp_buffer;
}();
// remaining methods look the same as in the macro version
}; For me, it looks cleaner than the macro version. I guess it's mainly because of having the formatted result in the BUT! As I already mentioned, this implementation is based on non-type template parameters that have some quirks. Here is an example: constexpr auto cf0 = FMT_COMPILE("{}{}");
// this works fine
constexpr auto cf1 = FMT_COMPILE_FORMAT(7, cf0, "foo", "bar");
// this doesn't compile because pointers to string literals are not allowed in template-parameters
constexpr auto cf1 = templaty_compiled_format<7, cf0, "foo", "bar">{};
// this may work, but `fixed_string` should become formattable first
using namespace fmt::detail_exported;
constexpr auto cf1 = templaty_compiled_format<7, cf0, fixed_string("foo"), fixed_string("bar")>{}; Also, non-type template parameters can be only literal types with additional restrictions, from cppreference:
So it really limits the types that could be passed as arguments for formatting. This way, some custom types would be unavailable, while the macro version theoretically works with them fine. Anyway, this templaty version already works just fine, but of course, with the same workarounds as the macro version does right now, and only for trivial types. |
Recently I tried to use {fmt} in compile-time, not some
FMT_STRING
compile-time checking orFMT_COMPILE
compile-time format string, but usingfmt::format_to
with arguments in compile-time, like this:In this example compiler should just format
"The answer = {}"
with argument42
intoformatted_string
array at compile-time. But since {fmt} probably was not planned to be used that way there are plenty of compilation errors with this example.By adding a
constexpr
keyword where it's needed, replacing somestd::
entities toconstexpr
ed self-written ones and using C++20std::is_constant_evaluated
to eliminate usages of non-constexpr
functions, I was able to format integers and strings into the buffer at compile-time. But then I tried to format a floating point number and realized that code for floating point formatting, besides of using maybe not compile-time friendly algorithms, uses dynamically growingmemory_buffer
.Despite I am pretty sure that I will be able to force floating point formatting to work at compile-time some time later, I decide to pause my work on this to get some feedback in advance.
So, will this ability be useful for everyone, or not? Because I was just experimenting, there is no need for me to do formatting in compile time. Are there any similar or worse pitfalls you can think of?
The text was updated successfully, but these errors were encountered: