A) Interlacing tfm::format
Partially in response to @ryanofsky's comment...
Potentially we could even extend the test to compare tinyformat behavior with ConstevalFormat behavior to make things even clearer, but just having a comment to clarify how these cases are treated by tinyformat would be very helpful.
...and partially out of my own curiosity about how tfm::format really behaves - I added tfm::format testing interlaced with the functionality for counting format specifiers.
It disproved my intuition about mismatching counts from: #30546 (review)
B) Free function?
As a consequence of wanting to avoid...
static_assert([]() { ConstevalFormatString<0>::Detail_CheckNumFormatSpecifiers(""); return true; }());
tfm::format("");
...I broke out a free function similarly to @stickies-v suggestion #30546 (review), enabling:
using util::detail::CountFormatSpecifiers;
static_assert(CountFormatSpecifiers("") == 0);
tfm::format("");
While I agree with maflcko that keeping functionality hard to use outside of the intended context has value, keeping tests straightforward is more important. If you prefer...
ConstevalFormatString<0>::Detail_CheckNumFormatSpecifiers("");
tfm::format("");
...I guess that could work too.
C) Dropped negatives
Also dropped some of the negative tests since the free 1 literal in...
static_assert(CountFormatSpecifiers("%1$s") == 1);
...more clearly and solely at compile-time covers both...
ConstevalFormatString<1>::Detail_CheckNumFormatSpecifiers("%1$s");
...and...
BOOST_CHECK_EXCEPTION(ConstevalFormatString<0>::Detail_CheckNumFormatSpecifiers("%1$s"), ErrType, check_num_spec);
BOOST_CHECK_EXCEPTION(ConstevalFormatString<2>::Detail_CheckNumFormatSpecifiers("%1$s"), ErrType, check_num_spec);
D) Split test function
Broke ConstevalFormatString_NumSpec test function into:
ConstevalFormatString_CorrectCounts
ConstevalFormatString_IncorrectCounts
ConstevalFormatString_NegativeChecks
E) Added parse_string_content tests
Added existing "tests" from run-lint-format-strings.py parse_string_content like @stickies-v did. Even if parity with tinyformat is more important than parity with a custom linter for tinyformat, I think it's still a nice touch.
<details>
<summary>
Compiling <code>git diff -w</code> on top of fa26462e95291652b4021d91b014655f678149e8
</summary>
diff --git a/src/test/util_string_tests.cpp b/src/test/util_string_tests.cpp
index c6ee1ffed9..2f6fad50b7 100644
--- a/src/test/util_string_tests.cpp
+++ b/src/test/util_string_tests.cpp
@@ -7,74 +7,172 @@
#include <boost/test/unit_test.hpp>
using namespace util;
+using util::detail::CountFormatSpecifiers;
+
+namespace {
+bool CheckTooMany(const tfm::format_error& s) { return s.what() == std::string_view{"tinyformat: Too many conversion specifiers in format string"}; }
+bool CheckOutOfRange(const tfm::format_error& s) { return s.what() == std::string_view{"tinyformat: Positional argument out of range"}; }
+}
BOOST_AUTO_TEST_SUITE(util_string_tests)
-BOOST_AUTO_TEST_CASE(ConstevalFormatString_NumSpec)
+// These are counted correctly, since tinyformat will require the provided number of args.
+BOOST_AUTO_TEST_CASE(ConstevalFormatString_CorrectCounts)
{
- // Compile-time sanity checks
- static_assert([] {
- ConstevalFormatString<0>::Detail_CheckNumFormatSpecifiers("");
- ConstevalFormatString<0>::Detail_CheckNumFormatSpecifiers("%%");
- ConstevalFormatString<1>::Detail_CheckNumFormatSpecifiers("%s");
- ConstevalFormatString<0>::Detail_CheckNumFormatSpecifiers("%%s");
- ConstevalFormatString<0>::Detail_CheckNumFormatSpecifiers("s%%");
- ConstevalFormatString<1>::Detail_CheckNumFormatSpecifiers("%%%s");
- ConstevalFormatString<1>::Detail_CheckNumFormatSpecifiers("%s%%");
- ConstevalFormatString<0>::Detail_CheckNumFormatSpecifiers(" 1$s");
- ConstevalFormatString<1>::Detail_CheckNumFormatSpecifiers("%1$s");
- ConstevalFormatString<1>::Detail_CheckNumFormatSpecifiers("%1$s%1$s");
- ConstevalFormatString<2>::Detail_CheckNumFormatSpecifiers("%2$s");
- ConstevalFormatString<2>::Detail_CheckNumFormatSpecifiers("%2$s 4$s %2$s");
- ConstevalFormatString<129>::Detail_CheckNumFormatSpecifiers("%129$s 999$s %2$s");
- ConstevalFormatString<1>::Detail_CheckNumFormatSpecifiers("%02d");
- ConstevalFormatString<1>::Detail_CheckNumFormatSpecifiers("%+2s");
- ConstevalFormatString<1>::Detail_CheckNumFormatSpecifiers("%+2s");
- ConstevalFormatString<1>::Detail_CheckNumFormatSpecifiers("%.6i");
- ConstevalFormatString<1>::Detail_CheckNumFormatSpecifiers("%5.2f");
- ConstevalFormatString<1>::Detail_CheckNumFormatSpecifiers("%#x");
- ConstevalFormatString<1>::Detail_CheckNumFormatSpecifiers("%1$5i");
- ConstevalFormatString<1>::Detail_CheckNumFormatSpecifiers("%1$-5i");
- ConstevalFormatString<1>::Detail_CheckNumFormatSpecifiers("%1$.5i");
- // tinyformat accepts almost any "type" spec, even '%', or '_', or '\n'.
- ConstevalFormatString<1>::Detail_CheckNumFormatSpecifiers("%123%");
- ConstevalFormatString<1>::Detail_CheckNumFormatSpecifiers("%123%s");
- ConstevalFormatString<1>::Detail_CheckNumFormatSpecifiers("%_");
- ConstevalFormatString<1>::Detail_CheckNumFormatSpecifiers("%\n");
- return true; // All checks above compiled and passed
- }());
- static_assert([] {
+ static_assert(CountFormatSpecifiers("") == 0);
+ tfm::format("");
+
+ static_assert(CountFormatSpecifiers("%%") == 0);
+ tfm::format("%%");
+
+ static_assert(CountFormatSpecifiers("%s") == 1);
+ BOOST_CHECK_EXCEPTION(tfm::format("%s"), tfm::format_error, CheckTooMany);
+ tfm::format("%s", "foo");
+
+ static_assert(CountFormatSpecifiers("%%s") == 0);
+ tfm::format("%%s");
+
+ static_assert(CountFormatSpecifiers("s%%") == 0);
+ tfm::format("s%%");
+
+ static_assert(CountFormatSpecifiers("%%%s") == 1);
+ BOOST_CHECK_EXCEPTION(tfm::format("%%%s"), tfm::format_error, CheckTooMany);
+ tfm::format("%%%s", "foo");
+
+ static_assert(CountFormatSpecifiers("%s%%") == 1);
+ BOOST_CHECK_EXCEPTION(tfm::format("%s%%"), tfm::format_error, CheckTooMany);
+ tfm::format("%s%%", "foo");
+
+ static_assert(CountFormatSpecifiers(" 1$s") == 0);
+ tfm::format(" 1$s");
+
+ static_assert(CountFormatSpecifiers("%1$s") == 1);
+ BOOST_CHECK_EXCEPTION(tfm::format("%1$s"), tfm::format_error, CheckOutOfRange);
+ tfm::format("%1$s", "foo");
+
+ static_assert(CountFormatSpecifiers("%1$s%1$s") == 1);
+ BOOST_CHECK_EXCEPTION(tfm::format("%1$s%1$s"), tfm::format_error, CheckOutOfRange);
+ tfm::format("%1$s%1$s", "foo");
+
+ static_assert(CountFormatSpecifiers("%2$s") == 2);
+ BOOST_CHECK_EXCEPTION(tfm::format("%2$s"), tfm::format_error, CheckOutOfRange);
+ BOOST_CHECK_EXCEPTION(tfm::format("%2$s", "foo"), tfm::format_error, CheckOutOfRange);
+ tfm::format("%2$s", "foo", "bar");
+
+ static_assert(CountFormatSpecifiers("%2$s 4$s %2$s") == 2);
+ BOOST_CHECK_EXCEPTION(tfm::format("%2$s 4$s %2$s"), tfm::format_error, CheckOutOfRange);
+ BOOST_CHECK_EXCEPTION(tfm::format("%2$s 4$s %2$s", "foo"), tfm::format_error, CheckOutOfRange);
+ tfm::format("%2$s 4$s %2$s", "foo", "bar");
+
+ static_assert(CountFormatSpecifiers("%12$s 99$s %2$s") == 12);
+ BOOST_CHECK_EXCEPTION(tfm::format("%12$s 99$s %2$s"), tfm::format_error, CheckOutOfRange);
+ BOOST_CHECK_EXCEPTION(tfm::format("%12$s 99$s %2$s", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11"), tfm::format_error, CheckOutOfRange);
+ tfm::format("%12$s 99$s %2$s", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12");
+
+ static_assert(CountFormatSpecifiers("%02d") == 1);
+ BOOST_CHECK_EXCEPTION(tfm::format("%02d"), tfm::format_error, CheckTooMany);
+ tfm::format("%02d", 1);
+
+ static_assert(CountFormatSpecifiers("%+2s") == 1);
+ BOOST_CHECK_EXCEPTION(tfm::format("%+2s"), tfm::format_error, CheckTooMany);
+ tfm::format("%+2s", 1);
+
+ static_assert(CountFormatSpecifiers("%.6i") == 1);
+ BOOST_CHECK_EXCEPTION(tfm::format("%.6i"), tfm::format_error, CheckTooMany);
+ tfm::format("%.6i", 1);
+
+ static_assert(CountFormatSpecifiers("%5.2f") == 1);
+ BOOST_CHECK_EXCEPTION(tfm::format("%5.2f"), tfm::format_error, CheckTooMany);
+ tfm::format("%5.2f", 1);
+
+ static_assert(CountFormatSpecifiers("%1$5i") == 1);
+ BOOST_CHECK_EXCEPTION(tfm::format("%1$5i"), tfm::format_error, CheckOutOfRange);
+ tfm::format("%1$5i", 1);
+
+ static_assert(CountFormatSpecifiers("%1$-5i") == 1);
+ BOOST_CHECK_EXCEPTION(tfm::format("%1$-5i"), tfm::format_error, CheckOutOfRange);
+ tfm::format("%1$-5i", 1);
+
+ static_assert(CountFormatSpecifiers("%1$.5i") == 1);
+ BOOST_CHECK_EXCEPTION(tfm::format("%1$.5i"), tfm::format_error, CheckOutOfRange);
+ tfm::format("%1$.5i", 1);
+
+ static_assert(CountFormatSpecifiers("%#x") == 1);
+ BOOST_CHECK_EXCEPTION(tfm::format("%#x"), tfm::format_error, CheckTooMany);
+ tfm::format("%#x", 1);
+
+ static_assert(CountFormatSpecifiers("%123%") == 1);
+ BOOST_CHECK_EXCEPTION(tfm::format("%123%"), tfm::format_error, CheckTooMany);
+ tfm::format("%123%", 1);
+
+ static_assert(CountFormatSpecifiers("%123%s") == 1);
+ BOOST_CHECK_EXCEPTION(tfm::format("%123%s"), tfm::format_error, CheckTooMany);
+ tfm::format("%123%s", 1);
+
+ static_assert(CountFormatSpecifiers("%_") == 1);
+ BOOST_CHECK_EXCEPTION(tfm::format("%_"), tfm::format_error, CheckTooMany);
+ tfm::format("%_", 1);
+
+ static_assert(CountFormatSpecifiers("%\n") == 1);
+ BOOST_CHECK_EXCEPTION(tfm::format("%\n"), tfm::format_error, CheckTooMany);
+ tfm::format("%\n", 1);
+}
+
// The `*` specifier behavior is unsupported and can lead to runtime
// errors when used in a ConstevalFormatString. Please refer to the
// note in the ConstevalFormatString docs.
- ConstevalFormatString<1>::Detail_CheckNumFormatSpecifiers("%*c");
- ConstevalFormatString<2>::Detail_CheckNumFormatSpecifiers("%2$*3$d");
- ConstevalFormatString<1>::Detail_CheckNumFormatSpecifiers("%.*f");
- return true; // All checks above compiled and passed
- }());
+BOOST_AUTO_TEST_CASE(ConstevalFormatString_IncorrectCounts)
+{
+ int a{}, b{}, c{};
- // Negative checks at runtime
+ auto check_not_enough{[](const tfm::format_error& s) { return s.what() == std::string_view{"tinyformat: Not enough arguments to read variable width or precision"}; }};
+ static_assert(CountFormatSpecifiers("%*c") == 1);
+ BOOST_CHECK_EXCEPTION(tfm::format("%*c"), tfm::format_error, check_not_enough);
+ BOOST_CHECK_EXCEPTION(tfm::format("%*c", a), tfm::format_error, CheckTooMany);
+ tfm::format("%*c", a, b);
+
+ static_assert(CountFormatSpecifiers("%2$*3$d") == 2);
+ BOOST_CHECK_EXCEPTION(tfm::format("%2$*3$d"), tfm::format_error, CheckOutOfRange);
+ BOOST_CHECK_EXCEPTION(tfm::format("%2$*3$d", a), tfm::format_error, CheckOutOfRange);
+ BOOST_CHECK_EXCEPTION(tfm::format("%2$*3$d", a, b), tfm::format_error, CheckOutOfRange);
+ tfm::format("%2$*3$d", a, b, c);
+
+ static_assert(CountFormatSpecifiers("%.*f") == 1);
+ BOOST_CHECK_EXCEPTION(tfm::format("%.*f"), tfm::format_error, check_not_enough);
+ BOOST_CHECK_EXCEPTION(tfm::format("%.*f", a), tfm::format_error, CheckTooMany);
+ tfm::format("%.*f", a, b);
+}
+
+// Negative checks. Executed at runtime as exceptions are not allowed at compile time.
+BOOST_AUTO_TEST_CASE(ConstevalFormatString_NegativeChecks)
+{
using ErrType = const char*;
auto check_mix{[](const ErrType& str) { return str == std::string_view{"Format specifiers must be all positional or all non-positional!"}; }};
- BOOST_CHECK_EXCEPTION(ConstevalFormatString<1>::Detail_CheckNumFormatSpecifiers("%s%1$s"), ErrType, check_mix);
-
- auto check_num_spec{[](const ErrType& str) { return str == std::string_view{"Format specifier count must match the argument count!"}; }};
- BOOST_CHECK_EXCEPTION(ConstevalFormatString<1>::Detail_CheckNumFormatSpecifiers(""), ErrType, check_num_spec);
- BOOST_CHECK_EXCEPTION(ConstevalFormatString<0>::Detail_CheckNumFormatSpecifiers("%s"), ErrType, check_num_spec);
- BOOST_CHECK_EXCEPTION(ConstevalFormatString<2>::Detail_CheckNumFormatSpecifiers("%s"), ErrType, check_num_spec);
- BOOST_CHECK_EXCEPTION(ConstevalFormatString<0>::Detail_CheckNumFormatSpecifiers("%1$s"), ErrType, check_num_spec);
- BOOST_CHECK_EXCEPTION(ConstevalFormatString<2>::Detail_CheckNumFormatSpecifiers("%1$s"), ErrType, check_num_spec);
+ BOOST_CHECK_EXCEPTION(CountFormatSpecifiers("%s%1$s"), ErrType, check_mix);
auto check_0_pos{[](const ErrType& str) { return str == std::string_view{"Positional format specifier must have position of at least 1"}; }};
- BOOST_CHECK_EXCEPTION(ConstevalFormatString<1>::Detail_CheckNumFormatSpecifiers("%$s"), ErrType, check_0_pos);
- BOOST_CHECK_EXCEPTION(ConstevalFormatString<1>::Detail_CheckNumFormatSpecifiers("%$"), ErrType, check_0_pos);
- BOOST_CHECK_EXCEPTION(ConstevalFormatString<0>::Detail_CheckNumFormatSpecifiers("%0$"), ErrType, check_0_pos);
- BOOST_CHECK_EXCEPTION(ConstevalFormatString<0>::Detail_CheckNumFormatSpecifiers("%0$s"), ErrType, check_0_pos);
+ BOOST_CHECK_EXCEPTION(CountFormatSpecifiers("%$s"), ErrType, check_0_pos);
+ BOOST_CHECK_EXCEPTION(CountFormatSpecifiers("%$"), ErrType, check_0_pos);
+ BOOST_CHECK_EXCEPTION(CountFormatSpecifiers("%0$"), ErrType, check_0_pos);
+ BOOST_CHECK_EXCEPTION(CountFormatSpecifiers("%0$s"), ErrType, check_0_pos);
auto check_term{[](const ErrType& str) { return str == std::string_view{"Format specifier incorrectly terminated by end of string"}; }};
- BOOST_CHECK_EXCEPTION(ConstevalFormatString<1>::Detail_CheckNumFormatSpecifiers("%"), ErrType, check_term);
- BOOST_CHECK_EXCEPTION(ConstevalFormatString<1>::Detail_CheckNumFormatSpecifiers("%1$"), ErrType, check_term);
+ BOOST_CHECK_EXCEPTION(CountFormatSpecifiers("%"), ErrType, check_term);
+ BOOST_CHECK_EXCEPTION(CountFormatSpecifiers("%1$"), ErrType, check_term);
+}
+
+// Existing "tests" in run-lint-format-strings.py parse_string_content
+BOOST_AUTO_TEST_CASE(ConstevalFormatString_run_lint_format_strings_parse_string_content)
+{
+ static_assert(CountFormatSpecifiers("foo bar foo") == 0);
+ static_assert(CountFormatSpecifiers("foo %d bar foo") == 1);
+ static_assert(CountFormatSpecifiers("foo %d bar %i foo") == 2);
+ static_assert(CountFormatSpecifiers("foo %d bar %i foo %% foo") == 2);
+ static_assert(CountFormatSpecifiers("foo %d bar %i foo %% foo %d foo") == 3);
+ //static_assert(CountFormatSpecifiers("foo %d bar %i foo %% foo %*d foo") == 4); // not implemented
+ static_assert(CountFormatSpecifiers("foo %5$d") == 5);
+ //static_assert(CountFormatSpecifiers("foo %5$*7$d") == 7); // not implemented
}
BOOST_AUTO_TEST_SUITE_END()
diff --git a/src/test/util_tests.cpp b/src/test/util_tests.cpp
index 1624fb8b5b..bd40d596ef 100644
--- a/src/test/util_tests.cpp
+++ b/src/test/util_tests.cpp
@@ -150,22 +150,24 @@ constexpr uint8_t HEX_PARSE_OUTPUT[] = {
static_assert((sizeof(HEX_PARSE_INPUT) - 1) == 2 * sizeof(HEX_PARSE_OUTPUT));
BOOST_AUTO_TEST_CASE(parse_hex)
{
+ using util::hex_literals::detail::Hex;
+
std::vector<unsigned char> result;
// Basic test vector
std::vector<unsigned char> expected(std::begin(HEX_PARSE_OUTPUT), std::end(HEX_PARSE_OUTPUT));
- constexpr std::array<std::byte, 65> hex_literal_array{operator""_hex<util::detail::Hex(HEX_PARSE_INPUT)>()};
+ constexpr std::array<std::byte, 65> hex_literal_array{operator""_hex<Hex(HEX_PARSE_INPUT)>()};
auto hex_literal_span{MakeUCharSpan(hex_literal_array)};
BOOST_CHECK_EQUAL_COLLECTIONS(hex_literal_span.begin(), hex_literal_span.end(), expected.begin(), expected.end());
- const std::vector<std::byte> hex_literal_vector{operator""_hex_v<util::detail::Hex(HEX_PARSE_INPUT)>()};
+ const std::vector<std::byte> hex_literal_vector{operator""_hex_v<Hex(HEX_PARSE_INPUT)>()};
hex_literal_span = MakeUCharSpan(hex_literal_vector);
BOOST_CHECK_EQUAL_COLLECTIONS(hex_literal_span.begin(), hex_literal_span.end(), expected.begin(), expected.end());
- constexpr std::array<uint8_t, 65> hex_literal_array_uint8{operator""_hex_u8<util::detail::Hex(HEX_PARSE_INPUT)>()};
+ constexpr std::array<uint8_t, 65> hex_literal_array_uint8{operator""_hex_u8<Hex(HEX_PARSE_INPUT)>()};
BOOST_CHECK_EQUAL_COLLECTIONS(hex_literal_array_uint8.begin(), hex_literal_array_uint8.end(), expected.begin(), expected.end());
- result = operator""_hex_v_u8<util::detail::Hex(HEX_PARSE_INPUT)>();
+ result = operator""_hex_v_u8<Hex(HEX_PARSE_INPUT)>();
BOOST_CHECK_EQUAL_COLLECTIONS(result.begin(), result.end(), expected.begin(), expected.end());
result = ParseHex(HEX_PARSE_INPUT);
diff --git a/src/util/strencodings.h b/src/util/strencodings.h
index 1543de03ab..b79806c251 100644
--- a/src/util/strencodings.h
+++ b/src/util/strencodings.h
@@ -427,16 +427,16 @@ struct Hex {
} // namespace detail
-template <util::detail::Hex str>
+template <util::hex_literals::detail::Hex str>
constexpr auto operator""_hex() { return str.bytes; }
-template <util::detail::Hex str>
+template <util::hex_literals::detail::Hex str>
constexpr auto operator""_hex_u8() { return std::bit_cast<std::array<uint8_t, str.bytes.size()>>(str.bytes); }
-template <util::detail::Hex str>
+template <util::hex_literals::detail::Hex str>
constexpr auto operator""_hex_v() { return std::vector<std::byte>{str.bytes.begin(), str.bytes.end()}; }
-template <util::detail::Hex str>
+template <util::hex_literals::detail::Hex str>
inline auto operator""_hex_v_u8() { return std::vector<uint8_t>{UCharCast(str.bytes.data()), UCharCast(str.bytes.data() + str.bytes.size())}; }
} // inline namespace hex_literals
diff --git a/src/util/string.h b/src/util/string.h
index 4724d881ff..d99c1963c6 100644
--- a/src/util/string.h
+++ b/src/util/string.h
@@ -18,25 +18,9 @@
#include <vector>
namespace util {
-/**
- * [@brief](/bitcoin-bitcoin/contributor/brief/) A wrapper for a compile-time partially validated format string
- *
- * This struct can be used to enforce partial compile-time validation of format
- * strings, to reduce the likelihood of tinyformat throwing exceptions at
- * run-time. Validation is partial to try and prevent the most common errors
- * while avoiding re-implementing the entire parsing logic.
- *
- * [@note](/bitcoin-bitcoin/contributor/note/) Counting of `*` dynamic width and precision fields (such as `%*c`,
- * `%2$*3$d`, `%.*f`) is not implemented to minimize code complexity as long as
- * they are not used in the codebase. Usage of these fields is not counted and
- * can lead to run-time exceptions. Code wanting to use the `*` specifier can
- * side-step this struct and call tinyformat directly.
- */
-template <unsigned num_params>
-struct ConstevalFormatString {
- const char* const fmt;
- consteval ConstevalFormatString(const char* str) : fmt{str} { Detail_CheckNumFormatSpecifiers(fmt); }
- constexpr static void Detail_CheckNumFormatSpecifiers(std::string_view str)
+
+namespace detail {
+constexpr static unsigned CountFormatSpecifiers(std::string_view str)
{
unsigned count_normal{0}; // Number of "normal" specifiers, like %s
unsigned count_pos{0}; // Max number in positional specifier, like %8$s
@@ -74,8 +58,31 @@ struct ConstevalFormatString {
// specifier is not checked. Parsing continues with the next '%'.
}
if (count_normal && count_pos) throw "Format specifiers must be all positional or all non-positional!";
- unsigned count{count_normal | count_pos};
- if (num_params != count) throw "Format specifier count must match the argument count!";
+ return count_normal | count_pos;
+}
+} // namespace detail
+
+/**
+ * [@brief](/bitcoin-bitcoin/contributor/brief/) A wrapper for a compile-time partially validated format string
+ *
+ * This struct can be used to enforce partial compile-time validation of format
+ * strings, to reduce the likelihood of tinyformat throwing exceptions at
+ * run-time. Validation is partial to try and prevent the most common errors
+ * while avoiding re-implementing the entire parsing logic.
+ *
+ * [@note](/bitcoin-bitcoin/contributor/note/) Counting of `*` dynamic width and precision fields (such as `%*c`,
+ * `%2$*3$d`, `%.*f`) is not implemented to minimize code complexity as long as
+ * they are not used in the codebase. Usage of these fields is not counted and
+ * can lead to run-time exceptions. Code wanting to use the `*` specifier can
+ * side-step this struct and call tinyformat directly.
+ */
+template <unsigned num_params>
+struct ConstevalFormatString {
+ const char* const fmt;
+ consteval ConstevalFormatString(const char* str) : fmt{str}
+ {
+ if (detail::CountFormatSpecifiers(fmt) != num_params)
+ throw "Format specifier count must match the argument count!";
}
};
(The unrelated changes in strencodings.h/util_tests.cpp are a consequence of clashing util::detail namespaces).
</details>