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

Basics of formatting at compile-time based on compile-time API #2019

Merged
merged 13 commits into from
Nov 29, 2020
Merged
18 changes: 9 additions & 9 deletions include/fmt/compile.h
Original file line number Diff line number Diff line change
Expand Up @@ -394,7 +394,7 @@ template <typename Char> struct text {
using char_type = Char;

template <typename OutputIt, typename... Args>
OutputIt format(OutputIt out, const Args&...) const {
constexpr OutputIt format(OutputIt out, const Args&...) const {
return write<Char>(out, data);
}
};
Expand All @@ -413,7 +413,7 @@ template <typename Char> struct code_unit {
using char_type = Char;

template <typename OutputIt, typename... Args>
OutputIt format(OutputIt out, const Args&...) const {
constexpr OutputIt format(OutputIt out, const Args&...) const {
return write<Char>(out, value);
}
};
Expand All @@ -426,7 +426,7 @@ template <typename Char, typename T, int N> struct field {
using char_type = Char;

template <typename OutputIt, typename... Args>
OutputIt format(OutputIt out, const Args&... args) const {
constexpr OutputIt format(OutputIt out, const Args&... args) const {
// This ensures that the argument type is convertile to `const T&`.
const T& arg = get<N>(args...);
return write<Char>(out, arg);
Expand Down Expand Up @@ -461,7 +461,7 @@ template <typename L, typename R> struct concat {
using char_type = typename L::char_type;

template <typename OutputIt, typename... Args>
OutputIt format(OutputIt out, const Args&... args) const {
constexpr OutputIt format(OutputIt out, const Args&... args) const {
out = lhs.format(out, args...);
return rhs.format(out, args...);
}
Expand Down Expand Up @@ -617,8 +617,8 @@ FMT_INLINE std::basic_string<Char> format(const CompiledFormat& cf,

template <typename OutputIt, typename CompiledFormat, typename... Args,
FMT_ENABLE_IF(detail::is_compiled_format<CompiledFormat>::value)>
OutputIt format_to(OutputIt out, const CompiledFormat& cf,
const Args&... args) {
constexpr OutputIt format_to(OutputIt out, const CompiledFormat& cf,
const Args&... args) {
return cf.format(out, args...);
}
# endif // __cpp_if_constexpr
Expand Down Expand Up @@ -654,8 +654,8 @@ FMT_INLINE std::basic_string<typename S::char_type> format(const S&,
template <typename OutputIt, typename CompiledFormat, typename... Args,
FMT_ENABLE_IF(std::is_base_of<detail::basic_compiled_format,
CompiledFormat>::value)>
OutputIt format_to(OutputIt out, const CompiledFormat& cf,
const Args&... args) {
constexpr OutputIt format_to(OutputIt out, const CompiledFormat& cf,
const Args&... args) {
using char_type = typename CompiledFormat::char_type;
using context = format_context_t<OutputIt, char_type>;
return detail::cf::vformat_to<context>(out, cf,
Expand All @@ -664,7 +664,7 @@ OutputIt format_to(OutputIt out, const CompiledFormat& cf,

template <typename OutputIt, typename S, typename... Args,
FMT_ENABLE_IF(detail::is_compiled_string<S>::value)>
OutputIt format_to(OutputIt out, const S&, const Args&... args) {
FMT_CONSTEXPR OutputIt format_to(OutputIt out, const S&, const Args&... args) {
constexpr auto compiled = detail::compile<Args...>(S());
return format_to(out, compiled, args...);
}
Expand Down
14 changes: 14 additions & 0 deletions include/fmt/core.h
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,12 @@
# define FMT_CONSTEXPR_DECL
#endif

#if __cplusplus >= 202002L
# define FMT_CONSTEXPR20 constexpr
#else
# define FMT_CONSTEXPR20 inline
#endif

#ifndef FMT_OVERRIDE
# if FMT_HAS_FEATURE(cxx_override_control) || \
(FMT_GCC_VERSION >= 408 && FMT_HAS_GXX_CXX11) || FMT_MSC_VER >= 1900
Expand Down Expand Up @@ -279,6 +285,14 @@ struct monostate {};

namespace detail {

constexpr bool is_constant_evaluated() FMT_DETECTED_NOEXCEPT {
#ifdef __cpp_lib_is_constant_evaluated
return std::is_constant_evaluated();
#else
return false;
#endif
}

// A helper function to suppress "conditional expression is constant" warnings.
template <typename T> constexpr T const_check(T value) { return value; }

Expand Down
70 changes: 45 additions & 25 deletions include/fmt/format.h
Original file line number Diff line number Diff line change
Expand Up @@ -390,7 +390,7 @@ inline buffer_appender<T> reserve(buffer_appender<T> it, size_t n) {
return it;
}

template <typename Iterator> inline Iterator& reserve(Iterator& it, size_t) {
template <typename Iterator> constexpr Iterator& reserve(Iterator& it, size_t) {
return it;
}

Expand All @@ -414,7 +414,7 @@ inline std::back_insert_iterator<Container> base_iterator(
}

template <typename Iterator>
inline Iterator base_iterator(Iterator, Iterator it) {
constexpr Iterator base_iterator(Iterator, Iterator it) {
return it;
}

Expand Down Expand Up @@ -587,14 +587,17 @@ using needs_conversion = bool_constant<

template <typename OutChar, typename InputIt, typename OutputIt,
FMT_ENABLE_IF(!needs_conversion<InputIt, OutChar>::value)>
OutputIt copy_str(InputIt begin, InputIt end, OutputIt it) {
FMT_CONSTEXPR OutputIt copy_str(InputIt begin, InputIt end, OutputIt it) {
while (begin != end) *it++ = *begin++;
return it;
}

template <typename OutChar, typename InputIt,
FMT_ENABLE_IF(!needs_conversion<InputIt, OutChar>::value)>
inline OutChar* copy_str(InputIt begin, InputIt end, OutChar* out) {
FMT_CONSTEXPR20 OutChar* copy_str(InputIt begin, InputIt end, OutChar* out) {
if (is_constant_evaluated()) {
return copy_str<OutChar, InputIt, OutChar*>(begin, end, out);
}
return std::uninitialized_copy(begin, end, out);
}

Expand Down Expand Up @@ -943,17 +946,7 @@ FMT_EXTERN template struct basic_data<void>;
// This is a struct rather than an alias to avoid shadowing warnings in gcc.
struct data : basic_data<> {};

#ifdef FMT_BUILTIN_CLZLL
// Returns the number of decimal digits in n. Leading zeros are not counted
// except for n == 0 in which case count_digits returns 1.
inline int count_digits(uint64_t n) {
// https://github.com/fmtlib/format-benchmark/blob/master/digits10
auto t = bsr2log10(FMT_BUILTIN_CLZLL(n | 1) ^ 63);
return t - (n < data::zero_or_powers_of_10_64_new[t]);
}
#else
// Fallback version of count_digits used when __builtin_clz is not available.
inline int count_digits(uint64_t n) {
template <typename T> FMT_CONSTEXPR int count_digits_fallback(T n) {
int count = 1;
for (;;) {
// Integer division is slow so do it for a group of four digits instead
Expand All @@ -967,10 +960,25 @@ inline int count_digits(uint64_t n) {
count += 4;
}
}

#ifdef FMT_BUILTIN_CLZLL
// Returns the number of decimal digits in n. Leading zeros are not counted
// except for n == 0 in which case count_digits returns 1.
FMT_CONSTEXPR20 int count_digits(uint64_t n) {
if (is_constant_evaluated()) {
return count_digits_fallback(n);
}
// https://github.com/fmtlib/format-benchmark/blob/master/digits10
auto t = bsr2log10(FMT_BUILTIN_CLZLL(n | 1) ^ 63);
return t - (n < data::zero_or_powers_of_10_64_new[t]);
}
#else
// Fallback version of count_digits used when __builtin_clz is not available.
FMT_CONSTEXPR int count_digits(uint64_t n) { return count_digits_fallback(n); }
#endif

#if FMT_USE_INT128
inline int count_digits(uint128_t n) {
FMT_CONSTEXPR int count_digits(uint128_t n) {
int count = 1;
for (;;) {
// Integer division is slow so do it for a group of four digits instead
Expand All @@ -987,7 +995,7 @@ inline int count_digits(uint128_t n) {
#endif

// Counts the number of digits in n. BITS = log2(radix).
template <unsigned BITS, typename UInt> inline int count_digits(UInt n) {
template <unsigned BITS, typename UInt> FMT_CONSTEXPR int count_digits(UInt n) {
int num_digits = 0;
do {
++num_digits;
Expand All @@ -1007,7 +1015,10 @@ template <> int count_digits<4>(detail::fallback_uintptr n);

#ifdef FMT_BUILTIN_CLZ
// Optional version of count_digits for better performance on 32-bit platforms.
inline int count_digits(uint32_t n) {
FMT_CONSTEXPR20 int count_digits(uint32_t n) {
if (is_constant_evaluated()) {
return count_digits_fallback(n);
}
auto t = bsr2log10(FMT_BUILTIN_CLZ(n | 1) ^ 31);
return t - (n < data::zero_or_powers_of_10_32_new[t]);
}
Expand Down Expand Up @@ -1067,11 +1078,20 @@ template <typename Iterator> struct format_decimal_result {
// buffer of specified size. The caller must ensure that the buffer is large
// enough.
template <typename Char, typename UInt>
inline format_decimal_result<Char*> format_decimal(Char* out, UInt value,
int size) {
FMT_CONSTEXPR20 format_decimal_result<Char*> format_decimal(Char* out,
UInt value,
int size) {
FMT_ASSERT(size >= count_digits(value), "invalid digit count");
out += size;
Char* end = out;
if (is_constant_evaluated()) {
while (value >= 10) {
*--out = static_cast<Char>('0' + value % 10);
value /= 10;
}
*--out = static_cast<Char>('0' + value);
return {out, end};
}
while (value >= 100) {
// Integer division is slow so do it for a group of two digits instead
// of for every digit. The idea comes from the talk by Alexandrescu
Expand Down Expand Up @@ -2049,7 +2069,7 @@ OutputIt write(OutputIt out, string_view value) {
}

template <typename Char, typename OutputIt>
OutputIt write(OutputIt out, basic_string_view<Char> value) {
FMT_CONSTEXPR OutputIt write(OutputIt out, basic_string_view<Char> value) {
auto it = reserve(out, value.size());
it = copy_str<Char>(value.begin(), value.end(), it);
return base_iterator(out, it);
Expand All @@ -2066,7 +2086,7 @@ template <typename Char, typename OutputIt, typename T,
FMT_ENABLE_IF(is_integral<T>::value &&
!std::is_same<T, bool>::value &&
!std::is_same<T, Char>::value)>
OutputIt write(OutputIt out, T value) {
FMT_CONSTEXPR OutputIt write(OutputIt out, T value) {
auto abs_value = static_cast<uint32_or_64_or_128_t<T>>(value);
bool negative = is_negative(value);
// Don't do -abs_value since it trips unsigned-integer-overflow sanitizer.
Expand All @@ -2085,19 +2105,19 @@ OutputIt write(OutputIt out, T value) {
}

template <typename Char, typename OutputIt>
OutputIt write(OutputIt out, bool value) {
constexpr OutputIt write(OutputIt out, bool value) {
return write<Char>(out, string_view(value ? "true" : "false"));
}

template <typename Char, typename OutputIt>
OutputIt write(OutputIt out, Char value) {
FMT_CONSTEXPR OutputIt write(OutputIt out, Char value) {
auto it = reserve(out, 1);
*it++ = value;
return base_iterator(out, it);
}

template <typename Char, typename OutputIt>
OutputIt write(OutputIt out, const Char* value) {
FMT_CONSTEXPR OutputIt write(OutputIt out, const Char* value) {
if (!value) {
FMT_THROW(format_error("string pointer is null"));
} else {
Expand Down
97 changes: 97 additions & 0 deletions test/compile-test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@

#include <string>
#include <type_traits>
#if __cplusplus >= 202002L
# include <string_view>
#endif

// Check that fmt/compile.h compiles with windows.h included before it.
#ifdef _WIN32
Expand Down Expand Up @@ -171,3 +174,97 @@ TEST(CompileTest, TextAndArg) {
EXPECT_EQ("42!", fmt::format(FMT_COMPILE("{}!"), 42));
}
#endif

#if __cplusplus >= 202002L
template <size_t max_string_length> struct constexpr_buffer_helper {
template <typename Func>
constexpr constexpr_buffer_helper& modify(Func func) {
func(buffer);
return *this;
}

template <typename T> constexpr bool operator==(const T& rhs) const noexcept {
return (std::string_view(rhs).compare(buffer.data()) == 0);
}

std::array<char, max_string_length> buffer{};
};
alexezeder marked this conversation as resolved.
Show resolved Hide resolved

TEST(CompileTimeFormattingTest, Bool) {
{
constexpr auto result =
constexpr_buffer_helper<5>{}.modify([](auto& buffer) {
fmt::format_to(buffer.data(), FMT_COMPILE("{}"), true);
});
EXPECT_EQ(result, "true");
}
{
constexpr auto result =
constexpr_buffer_helper<6>{}.modify([](auto& buffer) {
fmt::format_to(buffer.data(), FMT_COMPILE("{}"), false);
});
EXPECT_EQ(result, "false");
}
}

TEST(CompileTimeFormattingTest, Integer) {
{
constexpr auto result =
constexpr_buffer_helper<3>{}.modify([](auto& buffer) {
fmt::format_to(buffer.data(), FMT_COMPILE("{}"), 42);
});
EXPECT_EQ(result, "42");
}
{
constexpr auto result =
constexpr_buffer_helper<4>{}.modify([](auto& buffer) {
fmt::format_to(buffer.data(), FMT_COMPILE("{}"), 420);
});
EXPECT_EQ(result, "420");
}
{
constexpr auto result =
constexpr_buffer_helper<6>{}.modify([](auto& buffer) {
fmt::format_to(buffer.data(), FMT_COMPILE("{} {}"), 42, 42);
});
EXPECT_EQ(result, "42 42");
}
{
constexpr auto result =
constexpr_buffer_helper<6>{}.modify([=](auto& buffer) {
fmt::format_to(buffer.data(), FMT_COMPILE("{} {}"), uint32_t{42},
uint64_t{42});
});
EXPECT_EQ(result, "42 42");
}
}

TEST(CompileTimeFormattingTest, String) {
{
constexpr auto result =
constexpr_buffer_helper<3>{}.modify([](auto& buffer) {
fmt::format_to(buffer.data(), FMT_COMPILE("{}"), "42");
});
EXPECT_EQ(result, "42");
}
{
constexpr auto result =
constexpr_buffer_helper<17>{}.modify([](auto& buffer) {
fmt::format_to(buffer.data(), FMT_COMPILE("{} is {}"), "The answer",
"42");
});
EXPECT_EQ(result, "The answer is 42");
}
}

TEST(CompileTimeFormattingTest, Combination) {
{
alexezeder marked this conversation as resolved.
Show resolved Hide resolved
constexpr auto result =
constexpr_buffer_helper<18>{}.modify([](auto& buffer) {
fmt::format_to(buffer.data(), FMT_COMPILE("{}, {}, {}"), 420, true,
"answer");
});
EXPECT_EQ(result, "420, true, answer");
}
}
#endif