Skip to content

Commit

Permalink
Merge pull request #160 from CrustyAuklet/to_chars
Browse files Browse the repository at this point in the history
To chars
  • Loading branch information
cschreib authored May 2, 2024
2 parents ccd3afa + 4096a6c commit 81cce09
Show file tree
Hide file tree
Showing 8 changed files with 169 additions and 110 deletions.
1 change: 1 addition & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ set(SNITCH_WITH_MULTITHREADING ON CACHE BOOL "Make the testing fram
set(SNITCH_WITH_TIMINGS ON CACHE BOOL "Measure the time taken by each test case -- disable to speed up tests.")
set(SNITCH_WITH_SHORTHAND_MACROS ON CACHE BOOL "Use short names for test macros -- disable if this causes conflicts.")
set(SNITCH_CONSTEXPR_FLOAT_USE_BITCAST ON CACHE BOOL "Use std::bit_cast if available to implement exact constexpr float-to-string conversion.")
set(SNITCH_APPEND_TO_CHARS ON CACHE BOOL "Use std::to_chars for string conversions -- disable for greater compatability with a slight performance cost.")
set(SNITCH_DEFAULT_WITH_COLOR ON CACHE BOOL "Enable terminal colors by default -- can also be controlled by command line interface.")
set(SNITCH_DECOMPOSE_SUCCESSFUL_ASSERTIONS OFF CACHE BOOL "Enable expression decomposition even for successful assertions -- more expensive.")
set(SNITCH_WITH_ALL_REPORTERS ON CACHE BOOL "Allow all built-in reporters to be selected from the command line -- disable for faster compilation.")
Expand Down
49 changes: 28 additions & 21 deletions include/snitch/snitch_append.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -45,47 +45,53 @@ SNITCH_EXPORT [[nodiscard]] bool append_fast(small_string_span ss, double f) noe
return could_fit;
}

[[nodiscard]] constexpr std::size_t num_digits(large_uint_t x) noexcept {
return x >= 10u ? 1u + num_digits(x / 10u) : 1u;
template<large_uint_t Base = 10u, unsigned_integral T>
[[nodiscard]] constexpr std::size_t num_digits(T x) noexcept {
return x >= Base ? 1u + num_digits<Base>(x / Base) : 1u;
}

[[nodiscard]] constexpr std::size_t num_digits(large_int_t x) noexcept {
return x >= 10 ? 1u + num_digits(x / 10) : x <= -10 ? 1u + num_digits(x / 10) : x > 0 ? 1u : 2u;
template<large_int_t Base = 10, signed_integral T>
[[nodiscard]] constexpr std::size_t num_digits(T x) noexcept {
return (x >= Base || x <= -Base) ? 1u + num_digits<Base>(x / Base) : x > 0 ? 1u : 2u;
}

constexpr std::array<char, 10> digits = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9'};
constexpr std::array<char, 16> digits = {'0', '1', '2', '3', '4', '5', '6', '7',
'8', '9', 'a', 'b', 'c', 'd', 'e', 'f'};

constexpr std::size_t max_uint_length = num_digits(std::numeric_limits<large_uint_t>::max());
constexpr std::size_t max_int_length = max_uint_length + 1;

[[nodiscard]] constexpr bool append_constexpr(small_string_span ss, large_uint_t i) noexcept {
template<large_uint_t Base = 10u, unsigned_integral T>
[[nodiscard]] constexpr bool append_constexpr(small_string_span ss, T i) noexcept {
if (i != 0u) {
small_string<max_uint_length> tmp;
tmp.resize(num_digits(i));
tmp.resize(num_digits<Base>(i));
std::size_t k = 1;
for (large_uint_t j = i; j != 0u; j /= 10u, ++k) {
tmp[tmp.size() - k] = digits[j % 10u];
for (large_uint_t j = i; j != 0u; j /= Base, ++k) {
tmp[tmp.size() - k] = digits[j % Base];
}
return append_constexpr(ss, tmp);
} else {
return append_constexpr(ss, "0");
}
}

[[nodiscard]] constexpr bool append_constexpr(small_string_span ss, large_int_t i) noexcept {
template<large_int_t Base = 10, signed_integral T>
[[nodiscard]] constexpr bool append_constexpr(small_string_span ss, T i) noexcept {
if (i > 0) {
small_string<max_int_length> tmp;
tmp.resize(num_digits(i));
tmp.resize(num_digits<Base>(i));
std::size_t k = 1;
for (large_int_t j = i; j != 0; j /= 10, ++k) {
tmp[tmp.size() - k] = digits[j % 10];
for (large_int_t j = i; j != 0; j /= Base, ++k) {
tmp[tmp.size() - k] = digits[j % Base];
}
return append_constexpr(ss, tmp);
} else if (i < 0) {
small_string<max_int_length> tmp;
tmp.resize(num_digits(i));
tmp.resize(num_digits<Base>(i));
std::size_t k = 1;
for (large_int_t j = i; j != 0; j /= 10, ++k) {
tmp[tmp.size() - k] = digits[-(j % 10)];
for (large_int_t j = i; j != 0; j /= Base, ++k) {
tmp[tmp.size() - k] = digits[-(j % Base)];
}
tmp[0] = '-';
return append_constexpr(ss, tmp);
Expand All @@ -98,15 +104,15 @@ constexpr std::size_t max_int_length = max_uint_length + 1;
constexpr std::size_t min_exp_digits = 2u;

[[nodiscard]] constexpr std::size_t num_exp_digits(fixed_exp_t x) noexcept {
const std::size_t exp_digits = num_digits(static_cast<large_uint_t>(x > 0 ? x : -x));
const std::size_t exp_digits = num_digits<10>(static_cast<large_uint_t>(x > 0 ? x : -x));
return exp_digits < min_exp_digits ? min_exp_digits : exp_digits;
}

[[nodiscard]] constexpr std::size_t num_digits(const signed_fixed_data& x) noexcept {
// +1 for fractional separator '.'
// +1 for exponent separator 'e'
// +1 for exponent sign
return num_digits(static_cast<large_uint_t>(x.digits)) + num_exp_digits(x.exponent) +
return num_digits<10>(static_cast<large_uint_t>(x.digits)) + num_exp_digits(x.exponent) +
(x.sign ? 1u : 0u) + 3u;
}

Expand Down Expand Up @@ -135,7 +141,7 @@ set_precision(signed_fixed_data fd, std::size_t p) noexcept {
// and round-half-to-even is the default rounding mode for IEEE 754 floats. We don't follow
// the current rounding mode, but we can at least follow the default.

std::size_t base_digits = num_digits(static_cast<large_uint_t>(fd.digits));
std::size_t base_digits = num_digits<10>(static_cast<large_uint_t>(fd.digits));

bool only_zero = true;
while (base_digits > p) {
Expand All @@ -144,12 +150,13 @@ set_precision(signed_fixed_data fd, std::size_t p) noexcept {
only_zero = false;
}
fd.digits = fd.digits / 10u;
base_digits -= 1u;
} else {
fd.digits = round_half_to_even(fd.digits, only_zero);
fd.digits = round_half_to_even(fd.digits, only_zero);
base_digits = num_digits<10>(static_cast<large_uint_t>(fd.digits));
}

fd.exponent += 1;
base_digits -= 1u;
}

return fd;
Expand Down
11 changes: 9 additions & 2 deletions include/snitch/snitch_concepts.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,25 @@
#include <type_traits>

namespace snitch {

template<typename T>
concept integral = std::is_integral_v<T>;

template<typename T>
concept signed_integral = std::is_signed_v<T>;
concept signed_integral = integral<T> && std::is_signed_v<T>;

template<typename T>
concept unsigned_integral = std::is_unsigned_v<T>;
concept unsigned_integral = integral<T> && std::is_unsigned_v<T>;

template<typename T>
concept floating_point = std::is_floating_point_v<T>;

template<typename T, typename U>
concept convertible_to = std::is_convertible_v<T, U>;

template<typename T, typename U>
concept same_as = std::is_same_v<T, U>;

template<typename T>
concept enumeration = std::is_enum_v<T>;

Expand Down
10 changes: 10 additions & 0 deletions include/snitch/snitch_config.hpp.config
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,9 @@
#if !defined(SNITCH_CONSTEXPR_FLOAT_USE_BITCAST)
#cmakedefine01 SNITCH_CONSTEXPR_FLOAT_USE_BITCAST
#endif
#if !defined(SNITCH_APPEND_TO_CHARS)
#cmakedefine01 SNITCH_APPEND_TO_CHARS
#endif
#if !defined(SNITCH_DECOMPOSE_SUCCESSFUL_ASSERTIONS)
#cmakedefine01 SNITCH_DECOMPOSE_SUCCESSFUL_ASSERTIONS
#endif
Expand Down Expand Up @@ -111,6 +114,13 @@
# define SNITCH_CONSTEXPR_FLOAT_USE_BITCAST 0
#endif

#if (!defined(__cpp_lib_to_chars)) || (defined(_GLIBCXX_RELEASE) && _GLIBCXX_RELEASE <= 11) || \
(defined(_LIBCPP_VERSION) && _LIBCPP_VERSION <= 14000) || \
(defined(_MSC_VER) && _MSC_VER <= 1924)
# undef SNITCH_APPEND_TO_CHARS
# define SNITCH_APPEND_TO_CHARS 0
#endif

#if SNITCH_SHARED_LIBRARY
# if defined(_MSC_VER)
# if defined(SNITCH_EXPORTS)
Expand Down
1 change: 1 addition & 0 deletions meson_options.txt
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ option('with_multithreading' ,type: 'boolean', value: true, descript
option('with_timings' ,type: 'boolean' ,value: true, description: 'Measure the time taken by each test case -- disable to speed up tests.')
option('with_shorthand_macros' ,type: 'boolean' ,value: true, description: 'Use short names for test macros -- disable if this causes conflicts.')
option('constexpr_float_use_bitcast' ,type: 'boolean' ,value: true, description: 'Use std::bit_cast if available to implement exact constexpr float-to-string conversion.')
option('snitch_append_to_chars' ,type: 'boolean' ,value: true, description: 'Use std::to_chars for string conversions -- disable for greater compatability with a slight performance cost.')
option('default_with_color' ,type: 'boolean' ,value: true, description: 'Enable terminal colors by default -- can also be controlled by command line interface.')
option('decompose_successful_assertions' ,type: 'boolean' ,value: true, description: 'Enable expression decomposition even for successful assertions -- more expensive.')
option('with_all_reporters' ,type: 'boolean' ,value: true, description: 'Allow all built-in reporters to be selected from the command line -- disable for faster compilation.')
Expand Down
1 change: 1 addition & 0 deletions snitch/meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ conf_data = configuration_data({
'SNITCH_WITH_TIMINGS' : get_option('with_timings').to_int(),
'SNITCH_WITH_SHORTHAND_MACROS' : get_option('with_shorthand_macros').to_int(),
'SNITCH_CONSTEXPR_FLOAT_USE_BITCAST' : get_option('constexpr_float_use_bitcast').to_int(),
'SNITCH_APPEND_TO_CHARS' : get_option('snitch_append_to_chars').to_int(),
'SNITCH_DEFAULT_WITH_COLOR' : get_option('default_with_color').to_int(),
'SNITCH_DECOMPOSE_SUCCESSFUL_ASSERTIONS' : get_option('decompose_successful_assertions').to_int(),
'SNITCH_WITH_ALL_REPORTERS' : get_option('with_all_reporters').to_int(),
Expand Down
137 changes: 82 additions & 55 deletions src/snitch_append.cpp
Original file line number Diff line number Diff line change
@@ -1,65 +1,74 @@
#include "snitch/snitch_append.hpp"

#include <cinttypes> // for format strings
#include <cstdio> // for std::snprintf
#include "snitch/snitch_concepts.hpp"
#include "snitch/snitch_string.hpp"

#include <cstdint> // for std::uintptr_t
#include <cstring> // for std::memmove
#if SNITCH_APPEND_TO_CHARS
# include <charconv> // for std::to_chars
# include <system_error> // for std::errc
#endif

namespace snitch::impl {
namespace {
using snitch::small_string_span;

template<typename T>
constexpr const char* get_format_code() noexcept {
if constexpr (std::is_same_v<T, const void*>) {
#if defined(_MSC_VER)
return "0x%p";
#else
return "%p";
#endif
} else if constexpr (std::is_same_v<T, std::uintmax_t>) {
return "%" PRIuMAX;
} else if constexpr (std::is_same_v<T, std::intmax_t>) {
return "%" PRIdMAX;
} else if constexpr (std::is_same_v<T, float>) {
return "%.6e";
} else if constexpr (std::is_same_v<T, double>) {
return "%.15e";
} else {
static_assert(!std::is_same_v<T, T>, "unsupported type");
using namespace std::literals;

#if SNITCH_APPEND_TO_CHARS
template<floating_point T>
bool append_to(small_string_span ss, T value) noexcept {
constexpr auto fmt = std::chars_format::scientific;
constexpr auto precision = same_as<float, std::remove_cvref_t<T>> ? 6 : 15;
auto [end, err] = std::to_chars(ss.end(), ss.begin() + ss.capacity(), value, fmt, precision);
if (err != std::errc{}) {
// Not enough space, try into a temporary string that *should* be big enough,
// and copy whatever we can. 32 characters is enough for all integers and floating
// point values encoded on 64 bit or less.
small_string<32> fallback;
auto [end2, err2] = std::to_chars(
fallback.end(), fallback.begin() + fallback.capacity(), value, fmt, precision);
if (err2 != std::errc{}) {
return false;
}
fallback.grow(end2 - fallback.begin());
return append(ss, fallback);
}
}

template<typename T>
bool append_fmt(small_string_span ss, T value) noexcept {
if (ss.available() <= 1) {
// snprintf will always print a null-terminating character,
// so abort early if only space for one or zero character, as
// this would clobber the original string.
return false;
}
ss.grow(end - ss.end());
return true;
}

// Calculate required length.
const int return_code = std::snprintf(nullptr, 0, get_format_code<T>(), value);
if (return_code < 0) {
return false;
template<large_int_t Base = 10, integral T>
bool append_to(small_string_span ss, T value) noexcept {
auto [end, err] = std::to_chars(ss.end(), ss.begin() + ss.capacity(), value, Base);
if (err != std::errc{}) {
// Not enough space, try into a temporary string that *should* be big enough,
// and copy whatever we can. 32 characters is enough for all integers and floating
// point values encoded on 64 bit or less.
small_string<32> fallback;
auto [end2, err2] =
std::to_chars(fallback.end(), fallback.begin() + fallback.capacity(), value, Base);
if (err2 != std::errc{}) {
return false;
}
fallback.grow(end2 - fallback.begin());
return append(ss, fallback);
}
ss.grow(end - ss.end());
return true;
}
#else
template<floating_point T>
bool append_to(small_string_span ss, T value) noexcept {
return append_constexpr(ss, value);
}

// 'return_code' holds the number of characters that are required,
// excluding the null-terminating character, which always gets appended,
// so we need to +1.
const std::size_t length = static_cast<std::size_t>(return_code) + 1;
const bool could_fit = length <= ss.available();

const std::size_t offset = ss.size();
const std::size_t prev_space = ss.available();
ss.resize(std::min(ss.size() + length, ss.capacity()));
std::snprintf(ss.begin() + offset, prev_space, get_format_code<T>(), value);

// Pop the null-terminating character, always printed unfortunately.
ss.pop_back();

return could_fit;
template<large_int_t Base = 10, integral T>
bool append_to(small_string_span ss, T value) noexcept {
return append_constexpr<Base>(ss, value);
}
#endif
} // namespace

bool append_fast(small_string_span ss, std::string_view str) noexcept {
Expand All @@ -80,24 +89,42 @@ bool append_fast(small_string_span ss, std::string_view str) noexcept {
bool append_fast(small_string_span ss, const void* ptr) noexcept {
if (ptr == nullptr) {
return append(ss, nullptr);
} else {
return append_fmt(ss, ptr);
}

if (!append_fast(ss, "0x"sv)) {
return false;
}

const auto int_ptr = reinterpret_cast<std::uintptr_t>(ptr);

// Pad with zeros.
constexpr std::size_t max_digits = 2 * sizeof(void*);
std::size_t padding = max_digits - num_digits<16>(int_ptr);
while (padding > 0) {
constexpr std::string_view zeroes = "0000000000000000";
const std::size_t batch = std::min(zeroes.size(), padding);
if (!append_fast(ss, zeroes.substr(0, batch))) {
return false;
}

padding -= batch;
}
return append_to<16>(ss, int_ptr);
}

bool append_fast(small_string_span ss, large_uint_t i) noexcept {
return append_fmt(ss, i);
return append_to(ss, i);
}

bool append_fast(small_string_span ss, large_int_t i) noexcept {
return append_fmt(ss, i);
return append_to(ss, i);
}

bool append_fast(small_string_span ss, float f) noexcept {
return append_fmt(ss, f);
return append_to(ss, f);
}

bool append_fast(small_string_span ss, double d) noexcept {
return append_fmt(ss, d);
return append_to(ss, d);
}
} // namespace snitch::impl
Loading

0 comments on commit 81cce09

Please sign in to comment.