From 8257e277b162c7673452b0a0f0baed697d88cbe2 Mon Sep 17 00:00:00 2001 From: Sean Middleditch Date: Sat, 9 Oct 2021 23:56:28 -0700 Subject: [PATCH 1/4] Add format_append_to functions (#45) * Add format_append_to functions Also adds strnlen * Implement format_append_to_n * Add missing vformat_to * Fix missing start init --- docs/api.rst | 23 ++++++++++++++++- include/nanofmt/format.h | 22 +++++++++++++--- include/nanofmt/format.inl | 51 ++++++++++++++++++++++++++++++++++++++ tests/test_format.cpp | 12 +++++++++ 4 files changed, 104 insertions(+), 4 deletions(-) diff --git a/docs/api.rst b/docs/api.rst index 35aec8b..5e19abb 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -24,10 +24,18 @@ The :cpp:func:`nanofmt::format_to` functions format a given format string and arguments into the target buffer. The result will be NUL-terminated. The return value is a pointer to the terminating NUL character. +The :cpp:func:`nanofmt::format_append_to` functions format a given format +string and arguments onto the end of target buffer. The result will be NUL- +terminated. The return value is a pointer to the terminating NUL character. + .. cpp:function:: char* nanofmt::format_to(char (&dest)[N], format_string format_str, Args const&... args) .. cpp:function:: char* nanofmt::vformat_to(char (&dest)[N], format_string format_str, format_args args) +.. cpp:function:: char* nanofmt::format_append_to(char (&dest)[N], format_string format_str, Args const&... args) + +.. cpp:function:: char* nanofmt::vformat_append_to(char (&dest)[N], format_string format_str, format_args args) + Length-Delimited Formatting ^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -36,10 +44,19 @@ and arguments into the target buffer, up to the given number of characters. The result will **NOT** be NUL-terminated. The return value is a pointer to one past the last character written. +The :cpp:func:`nanofmt::format_append_to_n` functions format a given format +string and arguments onto the end of the target buffer, up to the given +number of characters. The result will **NOT** be NUL-terminated. The return +value is a pointer to one past the last character written. + .. cpp:function:: char* nanofmt::format_to_n(char* dest, std::size_t count, format_string format_str, Args const&... args) .. cpp:function:: char* nanofmt::vformat_to_n(char* dest, std::size_t count, format_string format_str, format_args&&) +.. cpp:function:: char* nanofmt::format_append_to_n(char* dest, std::size_t count, format_string format_str, Args const&... args) + +.. cpp:function:: char* nanofmt::vformat_append_to_n(char* dest, std::size_t count, format_string format_str, format_args&&) + Custom Formatters ^^^^^^^^^^^^^^^^^ @@ -156,7 +173,7 @@ General string utiltities that are useful in implementing formatting. Copies the provided character ``ch`` to the destination buffer, but not extending past the provided buffer end pointer. Returns the pointer past - the last character written. + the last character written. .. cpp:function:: char* fill_n(char* dest, char const* end, char ch, std::size_t count) noexcept @@ -164,6 +181,10 @@ General string utiltities that are useful in implementing formatting. but not extending past the provided buffer end pointer. Returns the pointer past the last character written. +.. cpp:function:: std::size_t strnlen(char const* buffer, std::size_t count) noexcept + + Returns the length of the string in buffer, to a maximum of count. + Format Strings ^^^^^^^^^^^^^^ diff --git a/include/nanofmt/format.h b/include/nanofmt/format.h index a66ac5d..8826460 100644 --- a/include/nanofmt/format.h +++ b/include/nanofmt/format.h @@ -54,6 +54,10 @@ namespace NANOFMT_NS { template struct formatter; + /// Overload to support converting user-defined string types to format_string. + template + constexpr format_string to_format_string(StringT const& value) noexcept; + // ---------------------- // String Utilities // ---------------------- @@ -82,9 +86,9 @@ namespace NANOFMT_NS { /// pointer past the last character written. [[nodiscard]] constexpr char* fill_n(char* dest, char const* end, char ch, std::size_t count) noexcept; - /// Overload to support converting user-defined string types to format_string. - template - constexpr format_string to_format_string(StringT const& value) noexcept; + /// Finds the first NUL character in the target buffer. Returns the length + /// of the buffer if no NUL is found. + [[nodiscard]] constexpr std::size_t strnlen(char const* buffer, std::size_t count) noexcept; // ---------------------- // Format API @@ -118,6 +122,18 @@ namespace NANOFMT_NS { [[nodiscard]] inline std::size_t vformat_length(format_string format_str, format_args args); + template + [[nodiscard]] char* format_append_to(char* dest, std::size_t count, format_string format_str, Args const&... args); + + template + [[nodiscard]] char* vformat_append_to(char* dest, std::size_t count, format_string format_str, format_args args); + + template + char* format_append_to(char (&dest)[N], format_string format_str, Args const&... args); + + template + char* vformat_append_to(char (&dest)[N], format_string format_str, format_args args); + // ---------------------- // Format Args // ---------------------- diff --git a/include/nanofmt/format.inl b/include/nanofmt/format.inl index a33e36a..b0b7571 100644 --- a/include/nanofmt/format.inl +++ b/include/nanofmt/format.inl @@ -176,6 +176,14 @@ namespace NANOFMT_NS { return copy_to_n(dest, end, pad_buffer, count); } + constexpr std::size_t strnlen(char const* buffer, std::size_t count) noexcept { + char const* const end = buffer + count; + for (char const* pos = buffer; pos != end; ++pos) + if (*pos == '\0') + return pos - buffer; + return count; + } + constexpr format_output& format_output::append(char const* const zstr) noexcept { char* const p = copy_to(pos, end, zstr); std::size_t const consumed = p - pos; @@ -271,6 +279,49 @@ namespace NANOFMT_NS { return pos; } + template + char* vformat_to(char (&dest)[N], format_string format_str, format_args args) { + char* const pos = detail::vformat(format_output{dest, dest + (N - 1 /*NUL*/)}, format_str, args).pos; + *pos = '\0'; + return pos; + } + + template + [[nodiscard]] char* format_append_to_n( + char* dest, + std::size_t count, + format_string format_str, + Args const&... args) { + std::size_t const start = ::NANOFMT_NS::strnlen(dest, N); + return detail::vformat( + format_output{dest + start, dest + count}, + format_str, + ::NANOFMT_NS::make_format_args(args...)) + .pos; + } + + template + [[nodiscard]] char* vformat_append_to_n(char* dest, std::size_t count, format_string format_str, format_args args) { + std::size_t const start = ::NANOFMT_NS::strnlen(dest, N); + return detail::vformat(format_output{dest + start, dest + count}, format_str, args).pos; + } + + template + char* format_append_to(char (&dest)[N], format_string format_str, Args const&... args) { + return vformat_append_to(dest, format_str, ::NANOFMT_NS::make_format_args(args...)); + } + + template + char* vformat_append_to(char (&dest)[N], format_string format_str, format_args args) { + std::size_t const start = ::NANOFMT_NS::strnlen(dest, N); + if (start == N) { + return dest + N; + } + char* const pos = detail::vformat(format_output{dest + start, dest + (N - 1 /*NUL*/)}, format_str, args).pos; + *pos = '\0'; + return pos; + } + template [[nodiscard]] std::size_t format_length(format_string format_str, Args const&... args) { return detail::vformat(format_output{}, format_str, ::NANOFMT_NS::make_format_args(args...)).advance; diff --git a/tests/test_format.cpp b/tests/test_format.cpp index 09b769e..48305a7 100644 --- a/tests/test_format.cpp +++ b/tests/test_format.cpp @@ -63,6 +63,18 @@ TEST_CASE("nanofmt.format.core", "[nanofmt][format]") { } } +TEST_CASE("nanofmt.format.append", "[nanofmt][format][append]") { + using namespace NANOFMT_NS; + + char buffer[12] = {}; + format_to(buffer, "Hello"); + format_append_to(buffer, "{} ", ','); + char* const end = format_append_to(buffer, "World! {:09d}", 9001); + + CHECK((end - buffer) == 11); + CHECK(std::strcmp(buffer, "Hello, Worl") == 0); +} + TEST_CASE("nanofmt.format.integers", "[nanofmt][format][integers]") { using namespace NANOFMT_NS::test; From c482342c1c0e7e236b24e2a83d92740a0424c791 Mon Sep 17 00:00:00 2001 From: Sean Middleditch Date: Sun, 10 Oct 2021 12:28:17 -0700 Subject: [PATCH 2/4] Use CMake add_test command (#47) * Use CMake add_test command * Fail test step if no tests are found * Actually enable testing :/ * Fix GCC to actually work * Fix Clang formatter is now always defined but has a deleted constructor for the default case. This is what fmtlib does, and... yeah, it simplifies the code tremendously. I'm distrustful of the overhead, but It Works(tm) is somewhat more important. --- .github/workflows/cmake.yml | 2 +- CMakeLists.txt | 1 + include/nanofmt/format.h | 5 +++++ include/nanofmt/format.inl | 14 +------------- tests/CMakeLists.txt | 3 +-- tests/test_format.cpp | 3 +++ tests/test_format_args.cpp | 8 +++++--- 7 files changed, 17 insertions(+), 19 deletions(-) diff --git a/.github/workflows/cmake.yml b/.github/workflows/cmake.yml index 7f11483..99dd291 100644 --- a/.github/workflows/cmake.yml +++ b/.github/workflows/cmake.yml @@ -42,7 +42,7 @@ jobs: - name: Test working-directory: ${{ github.workspace }}/build - run: ctest -C ${{ matrix.config }} + run: ctest --no-tests=error -C ${{ matrix.config }} - name: Install working-directory: ${{ github.workspace }}/build diff --git a/CMakeLists.txt b/CMakeLists.txt index 38e33b4..be4052b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -33,6 +33,7 @@ add_subdirectory(source) add_subdirectory(include/nanofmt) if (NANOFMT_TESTS) + enable_testing() add_subdirectory(tests) endif() diff --git a/include/nanofmt/format.h b/include/nanofmt/format.h index 8826460..6cd5017 100644 --- a/include/nanofmt/format.h +++ b/include/nanofmt/format.h @@ -169,6 +169,11 @@ namespace NANOFMT_NS { }; } // namespace detail + template + struct formatter { + formatter() = delete; // formatters must be default-constructible + }; + template <> struct formatter : detail::default_formatter {}; template <> diff --git a/include/nanofmt/format.inl b/include/nanofmt/format.inl index b0b7571..f837c93 100644 --- a/include/nanofmt/format.inl +++ b/include/nanofmt/format.inl @@ -17,8 +17,6 @@ namespace NANOFMT_NS { template constexpr format_arg make_format_arg(ValueT const& value) noexcept; - template - struct has_formatter; template struct value_type_map; @@ -332,18 +330,8 @@ namespace NANOFMT_NS { } namespace detail { - template - struct has_formatter { - static constexpr bool value = false; - }; template - struct has_formatter< - T, - std::void_t< - decltype(formatter{}.parse("", "")), - decltype(formatter{}.format(declval(), format_output{}))>> { - static constexpr bool value = true; - }; + using has_formatter = std::is_default_constructible<::NANOFMT_NS::formatter>; template struct value_type_map { diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 23db00a..3ca7951 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -12,5 +12,4 @@ target_link_libraries(nanofmt_test PRIVATE Catch2::Catch2WithMain ) -include(Catch) -catch_discover_tests(nanofmt_test) +add_test(NAME nanofmt_test COMMAND nanofmt_test) diff --git a/tests/test_format.cpp b/tests/test_format.cpp index 48305a7..5a05669 100644 --- a/tests/test_format.cpp +++ b/tests/test_format.cpp @@ -41,6 +41,9 @@ namespace NANOFMT_NS { }; } // namespace NANOFMT_NS +static_assert(NANOFMT_NS::detail::has_formatter::value, "has_formatter failed"); +static_assert(NANOFMT_NS::detail::has_formatter::value, "has_formatter failed"); + TEST_CASE("nanofmt.format.core", "[nanofmt][format]") { using namespace NANOFMT_NS; diff --git a/tests/test_format_args.cpp b/tests/test_format_args.cpp index a682c5b..d6f3706 100644 --- a/tests/test_format_args.cpp +++ b/tests/test_format_args.cpp @@ -80,14 +80,16 @@ TEST_CASE("nanofmt.format_arg.pointers", "[nanofmt][format_arg][pointers][string } } -TEST_CASE("nanofmt.format_arg.enums", "[nanofmt][format_arg][pointers][enums]") { - using namespace NANOFMT_NS; - +namespace { // clang-format off enum NANOFMT_GSL_SUPPRESS(enum.3) cenum { cenum_value }; // clang-format on enum class enum_class { value }; enum class chonky_enum_class : long long { value }; +} // namespace + +TEST_CASE("nanofmt.format_arg.enums", "[nanofmt][format_arg][pointers][enums]") { + using namespace NANOFMT_NS; CHECK(to_arg(cenum_value).tag == format_arg::type::t_int); CHECK(to_arg(enum_class::value).tag == format_arg::type::t_int); From ecff9a0c50bf00bfc177b4247bb95e9fe05ac678 Mon Sep 17 00:00:00 2001 From: Sean Middleditch Date: Sun, 10 Oct 2021 12:37:25 -0700 Subject: [PATCH 3/4] static_assert for unsupported format types (#46) --- include/nanofmt/format.inl | 5 ++++- tests/test_format.cpp | 18 +++++++++++------- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/include/nanofmt/format.inl b/include/nanofmt/format.inl index f837c93..0e4a206 100644 --- a/include/nanofmt/format.inl +++ b/include/nanofmt/format.inl @@ -333,6 +333,9 @@ namespace NANOFMT_NS { template using has_formatter = std::is_default_constructible<::NANOFMT_NS::formatter>; + template + constexpr bool always_false_v = false; + template struct value_type_map { using type = T; @@ -382,7 +385,7 @@ namespace NANOFMT_NS { return static_cast>::type>(value); } else { - return {}; + static_assert(always_false_v, "Type has no nanofmt::formatter<> specialization"); } } } // namespace detail diff --git a/tests/test_format.cpp b/tests/test_format.cpp index 5a05669..cab7645 100644 --- a/tests/test_format.cpp +++ b/tests/test_format.cpp @@ -14,6 +14,8 @@ struct custom_type { int value = 0; }; +struct unknown {}; + namespace NANOFMT_NS { template <> struct formatter : formatter { @@ -222,16 +224,18 @@ TEST_CASE("nanofmt.format.custom", "[nanofmt][format][custom]") { custom_type local{7}; custom_type& ref = local; + custom_type const clocal{7}; + custom_type const& cref = clocal; + CHECK(sformat("{}", custom_type{1}) == "custom{1}"); CHECK(sformat("{}", local) == "custom{7}"); CHECK(sformat("{}", ref) == "custom{7}"); + CHECK(sformat("{}", clocal) == "custom{7}"); + CHECK(sformat("{}", cref) == "custom{7}"); } -// SECTION("errors") { -// char buffer[256]; - -// CHECK(sformat_to(buffer, "{} {:4d} {:3.5f}", "abc", 9, 12.57) == ::NANOFMT_NS::format_result::success); -// CHECK(sformat_to(buffer, "{} {:4d", "abc", 9) == ::NANOFMT_NS::format_result::malformed_input); -// CHECK(sformat_to(buffer, "{0} {1}", "abc", 9) == ::NANOFMT_NS::format_result::success); -// CHECK(sformat_to(buffer, "{0} {1} {5}", "abc", 9, 12.57) == ::NANOFMT_NS::format_result::out_of_range); +// TEST_CASE("nanofmt.format.compile_error", "[nanofmt][format][error]") { +// using namespace NANOFMT_NS::test; +// +// CHECK(sformat("{}", unknown{}) == ""); //} From b979434328e1f0ec5e6ef977bc59a251c4bc9dfc Mon Sep 17 00:00:00 2001 From: Sean Middleditch Date: Sun, 10 Oct 2021 14:22:18 -0700 Subject: [PATCH 4/4] Add forward.h (#48) --- docs/api.rst | 24 ++++++++++++++++++++++++ include/nanofmt/CMakeLists.txt | 1 + include/nanofmt/config.h | 4 ++++ include/nanofmt/forward.h | 19 +++++++++++++++++++ tests/CMakeLists.txt | 2 +- tests/fwd_only_type.h | 25 +++++++++++++++++++++++++ tests/test_format.cpp | 3 +++ tests/test_utils.h | 2 ++ 8 files changed, 79 insertions(+), 1 deletion(-) create mode 100644 include/nanofmt/forward.h create mode 100644 tests/fwd_only_type.h diff --git a/docs/api.rst b/docs/api.rst index 5e19abb..319c045 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -14,6 +14,10 @@ Formatting The format API is available in the header ``nanofmt/format.h``. +The header ``nanofmt/forward.h`` offers forward declarations of +nanofmt types, including the ``formatter`` template that users +must specialize to support custom types. + Extensions for C++ standard library string types are in the header ``nanofmt/std_string.h``. @@ -80,6 +84,26 @@ specialized structure for nanofmt to work. Formats ``value`` to ``out``. +A header implementing a custom formatter may choose to only depend on +``nanofmt/foward.h`` header. This header does not offer any of the +implementations, nor does it provide declarations of the formatting +functions. A formatter may work around this by specifying the +``format_output&`` parameter of ``format`` as a template, as in: + +.. code-block:: c++ + + #include + + namespace nanofmt { + template<> + struct formatter { + constexpr char const* parse(char const* in, char const*) noexcept; + + template + void format(my_type const& value, OutputT& output); + } + } + Format Length ^^^^^^^^^^^^^ diff --git a/include/nanofmt/CMakeLists.txt b/include/nanofmt/CMakeLists.txt index 4af907a..ff187bb 100644 --- a/include/nanofmt/CMakeLists.txt +++ b/include/nanofmt/CMakeLists.txt @@ -3,5 +3,6 @@ target_sources(nanofmt PRIVATE "config.h" "format.h" "format.inl" + "forward.h" "std_string.h" ) diff --git a/include/nanofmt/config.h b/include/nanofmt/config.h index 66ec87c..8d01455 100644 --- a/include/nanofmt/config.h +++ b/include/nanofmt/config.h @@ -1,5 +1,7 @@ // Copyright (c) Sean Middleditch and contributors. See accompanying LICENSE.md for copyright details. +#ifndef NANOFMT_CONFIG_H_ +#define NANOFMT_CONFIG_H_ 1 #pragma once #if !defined(NANOFMT_NS) @@ -22,3 +24,5 @@ #if !defined(NANOFMT_GSL_SUPPRESS) # define NANOFMT_GSL_SUPPRESS(rule) #endif + +#endif diff --git a/include/nanofmt/forward.h b/include/nanofmt/forward.h new file mode 100644 index 0000000..431f4a5 --- /dev/null +++ b/include/nanofmt/forward.h @@ -0,0 +1,19 @@ +// Copyright (c) Sean Middleditch and contributors. See accompanying LICENSE.md for copyright details. + +#ifndef NANOFMT_FORWARD_H_ +#define NANOFMT_FORWARD_H_ 1 +#pragma once + +#include "config.h" + +namespace NANOFMT_NS { + struct format_arg; + struct format_args; + struct format_string; + struct format_string_view; + struct format_output; + template + struct formatter; +} // namespace NANOFMT_NS + +#endif diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 3ca7951..74e07cf 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -6,7 +6,7 @@ target_sources(nanofmt_test PRIVATE "test_format.cpp" "test_format_args.cpp" "test_utils.h" -) + "fwd_only_type.h") target_link_libraries(nanofmt_test PRIVATE nanofmt Catch2::Catch2WithMain diff --git a/tests/fwd_only_type.h b/tests/fwd_only_type.h new file mode 100644 index 0000000..b307db6 --- /dev/null +++ b/tests/fwd_only_type.h @@ -0,0 +1,25 @@ +// Copyright (c) Sean Middleditch and contributors. See accompanying LICENSE.md for copyright details. + +#pragma once + +#if defined(NANOFMT_FORMAT_H_) +# error "fwd_only_type.h should be included before format.h for the test to be correct" +#endif + +#include "nanofmt/forward.h" + +struct fwd_only_type {}; + +namespace NANOFMT_NS { + template <> + struct formatter { + constexpr char const* parse(char const* in, char const*) noexcept { + return in; + } + + template + void format(fwd_only_type, OutputT& output) { + output.append("fwd_only_type"); + } + }; +} // namespace NANOFMT_NS diff --git a/tests/test_format.cpp b/tests/test_format.cpp index cab7645..b1f936b 100644 --- a/tests/test_format.cpp +++ b/tests/test_format.cpp @@ -1,5 +1,6 @@ // Copyright (c) Sean Middleditch and contributors. See accompanying LICENSE.md for copyright details. +#include "fwd_only_type.h" #include "test_utils.h" #include "nanofmt/format.h" #include "nanofmt/std_string.h" @@ -232,6 +233,8 @@ TEST_CASE("nanofmt.format.custom", "[nanofmt][format][custom]") { CHECK(sformat("{}", ref) == "custom{7}"); CHECK(sformat("{}", clocal) == "custom{7}"); CHECK(sformat("{}", cref) == "custom{7}"); + + CHECK(sformat("{}", fwd_only_type{}) == "fwd_only_type"); } // TEST_CASE("nanofmt.format.compile_error", "[nanofmt][format][error]") { diff --git a/tests/test_utils.h b/tests/test_utils.h index 4515192..ecc1068 100644 --- a/tests/test_utils.h +++ b/tests/test_utils.h @@ -1,5 +1,7 @@ // Copyright (c) Sean Middleditch and contributors. See accompanying LICENSE.md for copyright details. +#pragma once + #include "nanofmt/charconv.h" #include "nanofmt/format.h"