Skip to content

Commit

Permalink
Improve compile-time formatting
Browse files Browse the repository at this point in the history
  • Loading branch information
AnthonyVH committed Aug 14, 2024
1 parent fb07b37 commit 9eb429f
Show file tree
Hide file tree
Showing 4 changed files with 187 additions and 55 deletions.
2 changes: 2 additions & 0 deletions include/fmt/compile.h
Original file line number Diff line number Diff line change
Expand Up @@ -467,6 +467,8 @@ FMT_INLINE std::basic_string<typename S::char_type> format(const S&,
template <typename OutputIt, typename S, typename... Args,
FMT_ENABLE_IF(detail::is_compiled_string<S>::value)>
FMT_CONSTEXPR OutputIt format_to(OutputIt out, const S&, Args&&... args) {
// Check that all types are formattable.
[[maybe_unused]] auto formattable_check = fmt::make_format_args(args...);
constexpr auto compiled = detail::compile<Args...>(S());
if constexpr (std::is_same<remove_cvref_t<decltype(compiled)>,
detail::unknown_format>()) {
Expand Down
27 changes: 20 additions & 7 deletions include/fmt/format.h
Original file line number Diff line number Diff line change
Expand Up @@ -970,7 +970,13 @@ template <typename Char, size_t N> struct fixed_string {
}
Char data[N] = {};
};
#endif

template <typename Char, size_t N>
constexpr auto format_as(fixed_string<Char, N> const& value)
-> basic_string_view<Char> {
return {value.data, N - 1};
}
#endif // FMT_USE_NONTYPE_TEMPLATE_ARGS

// Converts a compile-time string to basic_string_view.
template <typename Char, size_t N>
Expand Down Expand Up @@ -3624,11 +3630,13 @@ FMT_CONSTEXPR auto write(OutputIt out, const T& value) -> enable_if_t<

template <typename Char, typename OutputIt, typename T,
typename Context = basic_format_context<OutputIt, Char>>
FMT_CONSTEXPR auto write(OutputIt out, const T& value)
-> enable_if_t<mapped_type_constant<T, Context>::value ==
type::custom_type &&
!std::is_fundamental<T>::value,
OutputIt> {
FMT_CONSTEXPR auto write(OutputIt out, const T& value) -> enable_if_t<
mapped_type_constant<T, Context>::value == type::custom_type &&
!std::is_fundamental<T>::value &&
!std::is_same<
unformattable,
remove_cvref_t<decltype(arg_mapper<Context>().map(value))>>::value,
OutputIt> {
auto formatter = typename Context::template formatter_type<T>();
auto parse_ctx = typename Context::parse_context_type({});
formatter.parse(parse_ctx);
Expand Down Expand Up @@ -3879,7 +3887,8 @@ template <typename T, typename Char>
struct formatter<T, Char, enable_if_t<detail::has_format_as<T>::value>>
: formatter<detail::format_as_t<T>, Char> {
template <typename FormatContext>
auto format(const T& value, FormatContext& ctx) const -> decltype(ctx.out()) {
FMT_CONSTEXPR auto format(const T& value, FormatContext& ctx) const
-> decltype(ctx.out()) {
auto&& val = format_as(value); // Make an lvalue reference for format.
return formatter<detail::format_as_t<T>, Char>::format(val, ctx);
}
Expand Down Expand Up @@ -4235,6 +4244,10 @@ inline namespace literals {
* fmt::print("The answer is {answer}.", "answer"_a=42);
*/
# if FMT_USE_NONTYPE_TEMPLATE_ARGS
template <detail_exported::fixed_string Str> constexpr auto operator""_fs() {
return Str;
}

template <detail_exported::fixed_string Str> constexpr auto operator""_a() {
using char_t = remove_cvref_t<decltype(Str.data[0])>;
return detail::udl_arg<char_t, sizeof(Str.data) / sizeof(char_t), Str>();
Expand Down
193 changes: 145 additions & 48 deletions test/compile-test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
#include "fmt/ranges.h"
#include "gmock/gmock.h"
#include "gtest-extra.h"
#include "util.h"

TEST(compile_test, compile_fallback) {
// FMT_COMPILE should fallback on runtime formatting when `if constexpr` is
Expand Down Expand Up @@ -51,8 +52,8 @@ template <> struct formatter<test_formattable> : formatter<const char*> {
return it;
}
template <typename FormatContext>
constexpr auto format(test_formattable, FormatContext& ctx) const
-> decltype(ctx.out()) {
constexpr auto format(test_formattable,
FormatContext& ctx) const -> decltype(ctx.out()) {
return formatter<const char*>::format(word_spec == 'f' ? "foo" : "bar",
ctx);
}
Expand Down Expand Up @@ -292,7 +293,14 @@ TEST(compile_test, compile_format_string_literal) {
(!FMT_MSC_VERSION || \
(FMT_MSC_VERSION >= 1928 && FMT_MSC_VERSION < 1930)) && \
defined(__cpp_lib_is_constant_evaluated)
template <size_t max_string_length, typename Char = char> struct test_string {
# define FMT_TEST_CONSTEVAL_FORMAT_FIXED_LENGTH 1
#else
# define FMT_TEST_CONSTEVAL_FORMAT_FIXED_LENGTH 0
#endif

#if FMT_TEST_CONSTEVAL_FORMAT_FIXED_LENGTH
template <size_t max_string_length, typename Char = char>
struct fixed_length_string {
template <typename T> constexpr bool operator==(const T& rhs) const noexcept {
return fmt::basic_string_view<Char>(rhs).compare(buffer) == 0;
}
Expand All @@ -301,77 +309,166 @@ template <size_t max_string_length, typename Char = char> struct test_string {

template <size_t max_string_length, typename Char = char, typename... Args>
consteval auto test_format(auto format, const Args&... args) {
test_string<max_string_length, Char> string{};
fixed_length_string<max_string_length, Char> string{};
fmt::format_to(string.buffer, format, args...);
return string;
}
#endif

#if defined(__cpp_constexpr) && (__cpp_constexpr >= 202211L) && \
defined(FMT_CONSTEXPR20) && FMT_USE_NONTYPE_TEMPLATE_ARGS
# define FMT_TEST_CONSTEVAL_FORMAT_NTTP 1
#else
# define FMT_TEST_CONSTEVAL_FORMAT_NTTP 0
#endif

#if FMT_TEST_CONSTEVAL_FORMAT_NTTP
template <fmt::detail_exported::fixed_string format, auto... args>
constexpr std::string_view consteval_format() {
static constexpr auto str = [] {
using namespace fmt::literals;
// clang-format off
constexpr auto compiled_format = operator""_cf<format>();
// clang-format on
constexpr auto result_length =
fmt::formatted_size(compiled_format, args...);
auto result = std::array<char, result_length>{};
fmt::format_to(result.data(), compiled_format, args...);
return result;
}();
return std::string_view(str.data(), str.size());
}
#endif

TEST(compile_time_formatting_test, bool) {
EXPECT_EQ("true", test_format<5>(FMT_COMPILE("{}"), true));
EXPECT_EQ("false", test_format<6>(FMT_COMPILE("{}"), false));
EXPECT_EQ("true ", test_format<6>(FMT_COMPILE("{:5}"), true));
EXPECT_EQ("1", test_format<2>(FMT_COMPILE("{:d}"), true));
#if FMT_TEST_CONSTEVAL_FORMAT_FIXED_LENGTH || FMT_TEST_CONSTEVAL_FORMAT_NTTP
template <class CompileTimeAll>
struct compile_time_formatting_test : testing::Test {
using type = CompileTimeAll;
static constexpr auto value = type::value;
};

struct compile_time_formatting_test_name {
template <typename T> static std::string GetName(int) {
if constexpr (std::is_same_v<T, std::true_type>) {
return "nttp_format";
} else if constexpr (std::is_same_v<T, std::false_type>) {
return "fixed_length_format";
}
}
};

using compile_type_format_test_param_types = ::testing::Types<
# if FMT_TEST_CONSTEVAL_FORMAT_FIXED_LENGTH
std::false_type
# endif
# if FMT_TEST_CONSTEVAL_FORMAT_FIXED_LENGTH && FMT_TEST_CONSTEVAL_FORMAT_NTTP
,
# endif
# if FMT_TEST_CONSTEVAL_FORMAT_NTTP
std::true_type
# endif
>;
TYPED_TEST_SUITE(compile_time_formatting_test,
compile_type_format_test_param_types,
compile_time_formatting_test_name);

# define GENERATE_FORMAT_RESULT(length, format, ...) \
[] { \
constexpr bool use_nttp = TestFixture::type::value; \
if constexpr (use_nttp) { \
/* Convert char literals/arrays to fmt::detail_exported::fixed_string. \
* Pass other arguments through unmodified. \
*/ \
constexpr auto convert_char_array_to_fixed_string = [](auto&& arg) { \
using arg_type = std::remove_cvref_t<decltype(arg)>; \
using elem_type = std::remove_extent_t<arg_type>; \
if constexpr (std::is_bounded_array_v<arg_type> && \
std::is_same_v<char, elem_type>) { \
return fmt::detail_exported::fixed_string(arg); \
} else { \
return std::forward<decltype(arg)>(arg); \
} \
}; \
\
/* Can't pass char literals as NTTP, so convert those to \
* fmt::detail_exported::fixed_string before they're seen as template \
* arguments by the compiler, i.e. use a macro. \
*/ \
return consteval_format<format, \
FOR_EACH(convert_char_array_to_fixed_string, \
__VA_ARGS__)>(); \
} else { \
return test_format<length>(FMT_COMPILE(format) __VA_OPT__(, ) \
__VA_ARGS__); \
} \
}()

TYPED_TEST(compile_time_formatting_test, bool) {
EXPECT_EQ("false", (GENERATE_FORMAT_RESULT(6, "{}", false)));
EXPECT_EQ("true", (GENERATE_FORMAT_RESULT(5, "{}", true)));
EXPECT_EQ("true ", (GENERATE_FORMAT_RESULT(6, "{:5}", true)));
EXPECT_EQ("1", (GENERATE_FORMAT_RESULT(2, "{:d}", true)));
}

TEST(compile_time_formatting_test, integer) {
EXPECT_EQ("42", test_format<3>(FMT_COMPILE("{}"), 42));
EXPECT_EQ("420", test_format<4>(FMT_COMPILE("{}"), 420));
EXPECT_EQ("42 42", test_format<6>(FMT_COMPILE("{} {}"), 42, 42));
TYPED_TEST(compile_time_formatting_test, integer) {
EXPECT_EQ("42", (GENERATE_FORMAT_RESULT(3, "{}", 42)));
EXPECT_EQ("420", (GENERATE_FORMAT_RESULT(4, "{}", 420)));
EXPECT_EQ("42 42", (GENERATE_FORMAT_RESULT(6, "{} {}", 42, 42)));
EXPECT_EQ("42 42",
test_format<6>(FMT_COMPILE("{} {}"), uint32_t{42}, uint64_t{42}));
(GENERATE_FORMAT_RESULT(6, "{} {}", uint32_t{42}, uint64_t{42})));

EXPECT_EQ("+42", test_format<4>(FMT_COMPILE("{:+}"), 42));
EXPECT_EQ("42", test_format<3>(FMT_COMPILE("{:-}"), 42));
EXPECT_EQ(" 42", test_format<4>(FMT_COMPILE("{: }"), 42));
EXPECT_EQ("+42", (GENERATE_FORMAT_RESULT(4, "{:+}", 42)));
EXPECT_EQ("42", (GENERATE_FORMAT_RESULT(3, "{:-}", 42)));
EXPECT_EQ(" 42", (GENERATE_FORMAT_RESULT(4, "{: }", 42)));

EXPECT_EQ("-0042", test_format<6>(FMT_COMPILE("{:05}"), -42));
EXPECT_EQ("-0042", (GENERATE_FORMAT_RESULT(6, "{:05}", -42)));

EXPECT_EQ("101010", test_format<7>(FMT_COMPILE("{:b}"), 42));
EXPECT_EQ("0b101010", test_format<9>(FMT_COMPILE("{:#b}"), 42));
EXPECT_EQ("0B101010", test_format<9>(FMT_COMPILE("{:#B}"), 42));
EXPECT_EQ("042", test_format<4>(FMT_COMPILE("{:#o}"), 042));
EXPECT_EQ("0x4a", test_format<5>(FMT_COMPILE("{:#x}"), 0x4a));
EXPECT_EQ("0X4A", test_format<5>(FMT_COMPILE("{:#X}"), 0x4a));
EXPECT_EQ("101010", (GENERATE_FORMAT_RESULT(7, "{:b}", 42)));
EXPECT_EQ("0b101010", (GENERATE_FORMAT_RESULT(9, "{:#b}", 42)));
EXPECT_EQ("0B101010", (GENERATE_FORMAT_RESULT(9, "{:#B}", 42)));
EXPECT_EQ("042", (GENERATE_FORMAT_RESULT(4, "{:#o}", 042)));
EXPECT_EQ("0x4a", (GENERATE_FORMAT_RESULT(5, "{:#x}", 0x4a)));
EXPECT_EQ("0X4A", (GENERATE_FORMAT_RESULT(5, "{:#X}", 0x4a)));

EXPECT_EQ(" 42", test_format<6>(FMT_COMPILE("{:5}"), 42));
EXPECT_EQ(" 42", test_format<6>(FMT_COMPILE("{:5}"), 42ll));
EXPECT_EQ(" 42", test_format<6>(FMT_COMPILE("{:5}"), 42ull));
EXPECT_EQ(" 42", (GENERATE_FORMAT_RESULT(6, "{:5}", 42)));
EXPECT_EQ(" 42", (GENERATE_FORMAT_RESULT(6, "{:5}", 42ll)));
EXPECT_EQ(" 42", (GENERATE_FORMAT_RESULT(6, "{:5}", 42ull)));

EXPECT_EQ("42 ", test_format<5>(FMT_COMPILE("{:<4}"), 42));
EXPECT_EQ(" 42", test_format<5>(FMT_COMPILE("{:>4}"), 42));
EXPECT_EQ(" 42 ", test_format<5>(FMT_COMPILE("{:^4}"), 42));
EXPECT_EQ("**-42", test_format<6>(FMT_COMPILE("{:*>5}"), -42));
EXPECT_EQ("42 ", (GENERATE_FORMAT_RESULT(5, "{:<4}", 42)));
EXPECT_EQ(" 42", (GENERATE_FORMAT_RESULT(5, "{:>4}", 42)));
EXPECT_EQ(" 42 ", (GENERATE_FORMAT_RESULT(5, "{:^4}", 42)));
EXPECT_EQ("**-42", (GENERATE_FORMAT_RESULT(6, "{:*>5}", -42)));
}

TEST(compile_time_formatting_test, char) {
EXPECT_EQ("c", test_format<2>(FMT_COMPILE("{}"), 'c'));
TYPED_TEST(compile_time_formatting_test, char) {
EXPECT_EQ("c", (GENERATE_FORMAT_RESULT(2, "{}", 'c')));

EXPECT_EQ("c ", test_format<4>(FMT_COMPILE("{:3}"), 'c'));
EXPECT_EQ("99", test_format<3>(FMT_COMPILE("{:d}"), 'c'));
EXPECT_EQ("c ", (GENERATE_FORMAT_RESULT(4, "{:3}", 'c')));
EXPECT_EQ("99", (GENERATE_FORMAT_RESULT(3, "{:d}", 'c')));
}

TEST(compile_time_formatting_test, string) {
EXPECT_EQ("42", test_format<3>(FMT_COMPILE("{}"), "42"));
TYPED_TEST(compile_time_formatting_test, string) {
EXPECT_EQ("42", (GENERATE_FORMAT_RESULT(3, "{}", "42")));
EXPECT_EQ("The answer is 42",
test_format<17>(FMT_COMPILE("{} is {}"), "The answer", "42"));
(GENERATE_FORMAT_RESULT(17, "{} is {}", "The answer", "42")));

EXPECT_EQ("abc**", test_format<6>(FMT_COMPILE("{:*<5}"), "abc"));
EXPECT_EQ("**🤡**", test_format<9>(FMT_COMPILE("{:*^6}"), "🤡"));
EXPECT_EQ("abc**", (GENERATE_FORMAT_RESULT(6, "{:*<5}", "abc")));
EXPECT_EQ("**🤡**", (GENERATE_FORMAT_RESULT(9, "{:*^6}", "🤡")));
}

TEST(compile_time_formatting_test, combination) {
TYPED_TEST(compile_time_formatting_test, combination) {
EXPECT_EQ("420, true, answer",
test_format<18>(FMT_COMPILE("{}, {}, {}"), 420, true, "answer"));
(GENERATE_FORMAT_RESULT(18, "{}, {}, {}", 420, true, "answer")));

EXPECT_EQ(" -42", test_format<5>(FMT_COMPILE("{:{}}"), -42, 4));
EXPECT_EQ(" -42", (GENERATE_FORMAT_RESULT(5, "{:{}}", -42, 4)));
}

TEST(compile_time_formatting_test, custom_type) {
EXPECT_EQ("foo", test_format<4>(FMT_COMPILE("{}"), test_formattable()));
EXPECT_EQ("bar", test_format<4>(FMT_COMPILE("{:b}"), test_formattable()));
TYPED_TEST(compile_time_formatting_test, custom_type) {
EXPECT_EQ("foo", (GENERATE_FORMAT_RESULT(4, "{}", test_formattable{})));
EXPECT_EQ("bar", (GENERATE_FORMAT_RESULT(4, "{:b}", test_formattable{})));
}

TEST(compile_time_formatting_test, multibyte_fill) {
EXPECT_EQ("жж42", test_format<8>(FMT_COMPILE("{:ж>4}"), 42));
TYPED_TEST(compile_time_formatting_test, multibyte_fill) {
EXPECT_EQ("жж42", (GENERATE_FORMAT_RESULT(8, "{:ж>4}", 42)));
}
#endif
20 changes: 20 additions & 0 deletions test/util.h
Original file line number Diff line number Diff line change
Expand Up @@ -81,3 +81,23 @@ class date {
// otherwise.
auto get_locale(const char* name, const char* alt_name = nullptr)
-> std::locale;

// FOR_EACH macro that applies a given function to each variadic macro argument.
// FOREACH macro helpers
#define FOR_EACH_0(WHAT)
#define FOR_EACH_1(WHAT, X) WHAT(X)
#define FOR_EACH_2(WHAT, X, ...) WHAT(X), FOR_EACH_1(WHAT, __VA_ARGS__)
#define FOR_EACH_3(WHAT, X, ...) WHAT(X), FOR_EACH_2(WHAT, __VA_ARGS__)
#define FOR_EACH_4(WHAT, X, ...) WHAT(X), FOR_EACH_3(WHAT, __VA_ARGS__)
#define FOR_EACH_5(WHAT, X, ...) WHAT(X), FOR_EACH_4(WHAT, __VA_ARGS__)
#define FOR_EACH_6(WHAT, X, ...) WHAT(X), FOR_EACH_5(WHAT, __VA_ARGS__)
#define FOR_EACH_7(WHAT, X, ...) WHAT(X), FOR_EACH_6(WHAT, __VA_ARGS__)
#define FOR_EACH_8(WHAT, X, ...) WHAT(X), FOR_EACH_7(WHAT, __VA_ARGS__)
#define FOR_EACH_9(WHAT, X, ...) WHAT(X), FOR_EACH_8(WHAT, __VA_ARGS__)

#define FOR_EACH_GET_MACRO(_1, _2, _3, _4, _5, _6, _7, _8, _9, NAME, ...) NAME
#define FOR_EACH(action, ...) \
FOR_EACH_GET_MACRO(__VA_ARGS__ __VA_OPT__(, ) FOR_EACH_9, FOR_EACH_8, \
FOR_EACH_7, FOR_EACH_6, FOR_EACH_5, FOR_EACH_4, \
FOR_EACH_3, FOR_EACH_2, FOR_EACH_1, FOR_EACH_0) \
(action __VA_OPT__(, ) __VA_ARGS__)

0 comments on commit 9eb429f

Please sign in to comment.