Skip to content
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

Improve compile-time formatting #4118

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions include/fmt/compile.h
Original file line number Diff line number Diff line change
Expand Up @@ -414,6 +414,21 @@ constexpr auto compile(S fmt) {
}
}
#endif // defined(__cpp_if_constexpr) && defined(__cpp_return_type_deduction)

#if FMT_USE_NONTYPE_TEMPLATE_ARGS
// A wrapper for non-type template arguments that automatically encapsulates
// string literals, but leaves other argument types as-is.
template <typename T> struct nttp_arg {
template <typename U = T> constexpr nttp_arg(U const& arg) : arg(arg) {}
T arg;
};

template <typename T> nttp_arg(const T&) -> nttp_arg<T>;

template <typename Char, size_t N>
nttp_arg(const Char (&arg)[N])
-> nttp_arg<fmt::detail_exported::fixed_string<Char, N>>;
#endif // FMT_USE_NONTYPE_TEMPLATE_ARGS
} // namespace detail

FMT_BEGIN_EXPORT
Expand Down Expand Up @@ -467,6 +482,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 Expand Up @@ -520,6 +537,28 @@ template <detail_exported::fixed_string Str> constexpr auto operator""_cf() {
Str>();
}
} // namespace literals

# if FMT_USE_CONSTEVAL
// Can't be consteval due to llvm bug preventing static constexpr members in
// a consteval function: https://github.com/llvm/llvm-project/issues/82994.
template <detail_exported::fixed_string format_str, detail::nttp_arg... args>
constexpr auto format()
-> basic_string_view<remove_cvref_t<decltype(format_str.data[0])>> {
using char_t = remove_cvref_t<decltype(format_str.data[0])>;
static constexpr auto str = [] consteval {
// clang-format off
using namespace literals;
constexpr auto compiled_format = operator""_cf<format_str>();
// clang-format on
constexpr auto result_length =
formatted_size(compiled_format, (args.arg)...);
auto result = detail_exported::fixed_string<char_t, result_length>{};
format_to(result.data, compiled_format, (args.arg)...);
return result;
}();
return basic_string_view(str.data, sizeof(str.data) / sizeof(char_t));
}
# endif // FMT_USE_CONSTEVAL
#endif

FMT_END_EXPORT
Expand Down
43 changes: 29 additions & 14 deletions include/fmt/format.h
Original file line number Diff line number Diff line change
Expand Up @@ -964,13 +964,20 @@ class FMT_SO_VISIBILITY("default") format_error : public std::runtime_error {
namespace detail_exported {
#if FMT_USE_NONTYPE_TEMPLATE_ARGS
template <typename Char, size_t N> struct fixed_string {
constexpr fixed_string() = default;
constexpr fixed_string(const Char (&str)[N]) {
detail::copy<Char, const Char*, Char*>(static_cast<const Char*>(str),
str + N, data);
}
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 +3631,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,19 +3888,21 @@ 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);
}
};

#define FMT_FORMAT_AS(Type, Base) \
template <typename Char> \
struct formatter<Type, Char> : formatter<Base, Char> { \
template <typename FormatContext> \
auto format(Type value, FormatContext& ctx) const -> decltype(ctx.out()) { \
return formatter<Base, Char>::format(value, ctx); \
} \
#define FMT_FORMAT_AS(Type, Base) \
template <typename Char> \
struct formatter<Type, Char> : formatter<Base, Char> { \
template <typename FormatContext> \
FMT_CONSTEXPR auto format(Type value, FormatContext& ctx) const \
-> decltype(ctx.out()) { \
return formatter<Base, Char>::format(value, ctx); \
} \
}

FMT_FORMAT_AS(signed char, int);
Expand Down Expand Up @@ -4235,6 +4246,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
150 changes: 95 additions & 55 deletions test/compile-test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -288,90 +288,130 @@ TEST(compile_test, compile_format_string_literal) {
// (compiler file
// 'D:\a\_work\1\s\src\vctools\Compiler\CxxFE\sl\p1\c\constexpr\constexpr.cpp',
// line 8635)
#if FMT_USE_CONSTEVAL && \
(!FMT_MSC_VERSION || \
(FMT_MSC_VERSION >= 1928 && FMT_MSC_VERSION < 1930)) && \
// Can't support MSVC 19.28 & 19.29, because C++20 constexpr is required for
// fmt::v11::detail::buffer<T>::append.
#if FMT_USE_CONSTEVAL && (!FMT_MSC_VERSION || (FMT_MSC_VERSION >= 1939)) && \
defined(__cpp_lib_is_constant_evaluated)
template <size_t max_string_length, typename Char = char> struct test_string {
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;
}
Char buffer[max_string_length]{};
};

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{};
consteval auto fixed_length_format(auto format, const Args&... args) {
fixed_length_string<max_string_length, Char> string{};
fmt::detail::ignore_unused(fmt::formatted_size(format, args...));
fmt::format_to(string.buffer, format, args...);
return string;
}

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));
}

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));
EXPECT_EQ("42 42",
test_format<6>(FMT_COMPILE("{} {}"), 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("-0042", test_format<6>(FMT_COMPILE("{:05}"), -42));
template <class UseNTTPFormat>
struct compile_time_formatting_test : testing::Test {
static constexpr bool use_nttp_format = UseNTTPFormat::value;
};

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));
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";
}
}
};

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));
// clang-format off
using compile_type_format_test_param_types = ::testing::Types<std::false_type
# if FMT_USE_NONTYPE_TEMPLATE_ARGS
, std::true_type
# endif // FMT_USE_NONTYPE_TEMPLATE_ARGS
>;
// clang-format on

TYPED_TEST_SUITE(compile_time_formatting_test,
compile_type_format_test_param_types,
compile_time_formatting_test_name);

# define GEN_FMT_COMPILE_RESULT(length, format_str, ...) \
[] { \
if constexpr (TestFixture::use_nttp_format) { \
return fmt::format<format_str __VA_OPT__(, ) __VA_ARGS__>(); \
} else { \
return fixed_length_format<length>(FMT_COMPILE(format_str) \
__VA_OPT__(, ) __VA_ARGS__); \
} \
}()

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

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));
TYPED_TEST(compile_time_formatting_test, integer) {
EXPECT_EQ("42", (GEN_FMT_COMPILE_RESULT(3, "{}", 42)));
EXPECT_EQ("420", (GEN_FMT_COMPILE_RESULT(4, "{}", 420)));
EXPECT_EQ("42 42", (GEN_FMT_COMPILE_RESULT(6, "{} {}", 42, 42)));
EXPECT_EQ("42 42",
(GEN_FMT_COMPILE_RESULT(6, "{} {}", uint32_t{42}, uint64_t{42})));

EXPECT_EQ("+42", (GEN_FMT_COMPILE_RESULT(4, "{:+}", 42)));
EXPECT_EQ("42", (GEN_FMT_COMPILE_RESULT(3, "{:-}", 42)));
EXPECT_EQ(" 42", (GEN_FMT_COMPILE_RESULT(4, "{: }", 42)));

EXPECT_EQ("-0042", (GEN_FMT_COMPILE_RESULT(6, "{:05}", -42)));

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

EXPECT_EQ(" 42", (GEN_FMT_COMPILE_RESULT(6, "{:5}", 42)));
EXPECT_EQ(" 42", (GEN_FMT_COMPILE_RESULT(6, "{:5}", 42l)));
EXPECT_EQ(" 42", (GEN_FMT_COMPILE_RESULT(6, "{:5}", 42ll)));
EXPECT_EQ(" 42", (GEN_FMT_COMPILE_RESULT(6, "{:5}", 42ull)));

EXPECT_EQ("42 ", (GEN_FMT_COMPILE_RESULT(5, "{:<4}", 42)));
EXPECT_EQ(" 42", (GEN_FMT_COMPILE_RESULT(5, "{:>4}", 42)));
EXPECT_EQ(" 42 ", (GEN_FMT_COMPILE_RESULT(5, "{:^4}", 42)));
EXPECT_EQ("**-42", (GEN_FMT_COMPILE_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", (GEN_FMT_COMPILE_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 ", (GEN_FMT_COMPILE_RESULT(4, "{:3}", 'c')));
EXPECT_EQ("99", (GEN_FMT_COMPILE_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", (GEN_FMT_COMPILE_RESULT(3, "{}", "42")));
EXPECT_EQ("The answer is 42",
test_format<17>(FMT_COMPILE("{} is {}"), "The answer", "42"));
(GEN_FMT_COMPILE_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**", (GEN_FMT_COMPILE_RESULT(6, "{:*<5}", "abc")));
EXPECT_EQ("**🤡**", (GEN_FMT_COMPILE_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"));
(GEN_FMT_COMPILE_RESULT(18, "{}, {}, {}", 420, true, "answer")));

EXPECT_EQ(" -42", test_format<5>(FMT_COMPILE("{:{}}"), -42, 4));
EXPECT_EQ(" -42", (GEN_FMT_COMPILE_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", (GEN_FMT_COMPILE_RESULT(4, "{}", test_formattable{})));
EXPECT_EQ("bar", (GEN_FMT_COMPILE_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", (GEN_FMT_COMPILE_RESULT(8, "{:ж>4}", 42)));
}
#endif