From 71bc0e828c37f8e91a9e496bb1ad15219157543c Mon Sep 17 00:00:00 2001 From: Toyosatomimi no Miko <110693261+mikomikotaishi@users.noreply.github.com> Date: Sun, 26 Jul 2026 16:10:45 -0400 Subject: [PATCH] Add opt-in support for custom string types --- README.md | 2 +- fuzzing/build.sh | 1 + include/toml++/impl/forward_declarations.hpp | 87 ++++- include/toml++/impl/value.hpp | 24 +- src/modules/tomlplusplus.cppm | 1 + tests/custom_string.cpp | 338 ++++++++++++++++++ tests/meson.build | 1 + tests/vs/test_debug_x64.vcxproj | 1 + tests/vs/test_debug_x64_cpplatest.vcxproj | 1 + .../test_debug_x64_cpplatest_noexcept.vcxproj | 1 + ...debug_x64_cpplatest_noexcept_unrel.vcxproj | 1 + .../vs/test_debug_x64_cpplatest_unrel.vcxproj | 1 + tests/vs/test_debug_x64_noexcept.vcxproj | 1 + .../vs/test_debug_x64_noexcept_unrel.vcxproj | 1 + tests/vs/test_debug_x64_unrel.vcxproj | 1 + tests/vs/test_debug_x86.vcxproj | 1 + tests/vs/test_debug_x86_cpplatest.vcxproj | 1 + .../test_debug_x86_cpplatest_noexcept.vcxproj | 1 + ...debug_x86_cpplatest_noexcept_unrel.vcxproj | 1 + .../vs/test_debug_x86_cpplatest_unrel.vcxproj | 1 + tests/vs/test_debug_x86_noexcept.vcxproj | 1 + .../vs/test_debug_x86_noexcept_unrel.vcxproj | 1 + tests/vs/test_debug_x86_unrel.vcxproj | 1 + tests/vs/test_release_x64.vcxproj | 1 + tests/vs/test_release_x64_cpplatest.vcxproj | 1 + ...est_release_x64_cpplatest_noexcept.vcxproj | 1 + ...lease_x64_cpplatest_noexcept_unrel.vcxproj | 1 + .../test_release_x64_cpplatest_unrel.vcxproj | 1 + tests/vs/test_release_x64_noexcept.vcxproj | 1 + .../test_release_x64_noexcept_unrel.vcxproj | 1 + tests/vs/test_release_x64_unrel.vcxproj | 1 + tests/vs/test_release_x86.vcxproj | 1 + tests/vs/test_release_x86_cpplatest.vcxproj | 1 + ...est_release_x86_cpplatest_noexcept.vcxproj | 1 + ...lease_x86_cpplatest_noexcept_unrel.vcxproj | 1 + .../test_release_x86_cpplatest_unrel.vcxproj | 1 + tests/vs/test_release_x86_noexcept.vcxproj | 1 + .../test_release_x86_noexcept_unrel.vcxproj | 1 + tests/vs/test_release_x86_unrel.vcxproj | 1 + toml.hpp | 84 ++++- tools/generate_windows_test_targets.py | 1 + 41 files changed, 564 insertions(+), 7 deletions(-) create mode 100644 tests/custom_string.cpp diff --git a/README.md b/README.md index 08403aac..1915c124 100644 --- a/README.md +++ b/README.md @@ -294,7 +294,7 @@ UTF-8 decoding is performed using a state machine based on Bjoern Hoehrmann's '[ - **[@jonestristand](https://github.com/jonestristand)** - Designed and implemented the `toml::path`s feature - **[@kcsaul](https://github.com/kcsaul)** - Fixed a bug - **[@levicki](https://github.com/levicki)** - Helped design some new features -- **[@mikomikotaishi](https://github.com/mikomikotaishi)** - Added support for C++20 modules +- **[@mikomikotaishi](https://github.com/mikomikotaishi)** - Added support for C++20 modules and custom strings - **[@moorereason](https://github.com/moorereason)** - Reported a whole bunch of bugs - **[@mosra](https://github.com/mosra)** - Created the awesome [m.css] used to generate the API docs - **[@N-Dekker](https://github.com/N-Dekker)** - Added a workaround for the legacy lambda processor of MSVC 2019/2022, added `get_line` diff --git a/fuzzing/build.sh b/fuzzing/build.sh index 3fce82ae..554ced2a 100755 --- a/fuzzing/build.sh +++ b/fuzzing/build.sh @@ -15,6 +15,7 @@ clang++ -std=c++17 -O2 -DUSE_VENDORED_LIBS=1 \ tests/conformance_burntsushi_valid.cpp \ tests/conformance_iarna_invalid.cpp \ tests/conformance_iarna_valid.cpp \ + tests/custom_string.cpp \ tests/formatters.cpp \ tests/for_each.cpp \ tests/impl_toml.cpp \ diff --git a/include/toml++/impl/forward_declarations.hpp b/include/toml++/impl/forward_declarations.hpp index f4dfe4ff..a0c88a39 100644 --- a/include/toml++/impl/forward_declarations.hpp +++ b/include/toml++/impl/forward_declarations.hpp @@ -379,6 +379,37 @@ TOML_NAMESPACE_START // abi namespace /// \brief The 'default' formatter used by TOML objects when they are printed to a stream. /// \detail This is an alias for #toml::toml_formatter. using default_formatter = toml_formatter; + + /// \brief Customization point for using your own string types with toml++. + /// + /// \detail Specialize this for a user-defined string type to opt it in to being usable as an initializer + /// for TOML string values, and as a target type for node::value(), node::value_or() and friends: \cpp + /// namespace toml + /// { + /// template <> + /// struct string_like : std::true_type + /// {}; + /// } + /// + /// // ... thereafter: + /// tbl.insert("hostname", foo::String{ "localhost" }); + /// auto hostname = tbl["hostname"].value(); + /// \ecpp + /// + /// Opting in is necessary but not sufficient; the direction(s) in which a type may be used are + /// determined by the conversions it actually supports: + /// - to be usable as an initializer, it must be convertible to `std::string_view` + /// - to be usable as a retrieval target, it must be constructible from `std::string_view` + /// + /// Types satisfying only one of the two are supported in that direction only; attempting to use one + /// in the unsupported direction is a compile error. + /// + /// \note TOML string values are always stored internally as std::string. This customization point governs + /// conversions at the API boundary only, not the underlying storage, so retrieving a value as a + /// user-defined string type makes a copy. + template + struct string_like : std::false_type + {}; } TOML_NAMESPACE_END; @@ -407,12 +438,42 @@ TOML_IMPL_NAMESPACE_START inline constexpr bool is_wide_string = is_one_of, const wchar_t*, wchar_t*, std::wstring_view, std::wstring>; + // user-defined string types (see toml::string_like) + + template + inline constexpr bool is_string_like = toml::string_like>::value; + + // ... usable as an initializer for a TOML string value. + // note that a type converting via operator std::string() is *not* convertible to std::string_view (that would + // require two user-defined conversions), so both targets have to be checked independently. + template + inline constexpr bool string_like_is_initializer = + is_string_like + && (std::is_convertible_v&, std::string_view> + || std::is_convertible_v&, std::string>); + + // ... usable as a target type when retrieving a TOML string value + template + inline constexpr bool string_like_is_target = is_string_like + && (std::is_constructible_v, std::string_view> + || std::is_constructible_v, const std::string&>); + + // ... and whether constructing that target can throw (it is built from whichever of the two the type supports, + // preferring the view since it never allocates a temporary) + template + inline constexpr bool string_like_target_is_nothrow = + std::is_constructible_v, std::string_view> + ? std::is_nothrow_constructible_v, std::string_view> + : std::is_nothrow_constructible_v, const std::string&>; + template inline constexpr bool value_retrieval_is_nothrow = !std::is_same_v, std::string> #if TOML_HAS_CHAR8 && !std::is_same_v, std::u8string> #endif - + // user-defined string types are constructed from the stored + // std::string, so they can throw iff that construction can + && !(string_like_is_target && !string_like_target_is_nothrow) && !is_wide_string; template @@ -507,12 +568,17 @@ TOML_IMPL_NAMESPACE_START template struct value_traits; + // (defined alongside the other string traits, below) + template + struct user_string_traits; + + // note: enums cannot have conversion operators, so the enum and string-like cases are mutually exclusive template > struct value_traits_base_selector { static_assert(!is_cvref); - using type = default_value_traits; + using type = std::conditional_t, user_string_traits, default_value_traits>; }; template struct value_traits_base_selector @@ -742,6 +808,23 @@ TOML_IMPL_NAMESPACE_START struct value_traits : string_traits {}; + // string value_traits specializations - user-defined string types (see toml::string_like) + // + // unlike the built-in string types these are never 'native' (storage is always std::string), and the two + // directions are tracked independently, since a type may support only one of them: + // - is_losslessly_convertible_to_native => usable as an initializer (T -> std::string_view) + // - can_represent_native => usable as a retrieval target (std::string_view -> T) + template + struct user_string_traits + { + using native_type = std::string; + static constexpr bool is_native = false; + static constexpr bool is_losslessly_convertible_to_native = string_like_is_initializer; + static constexpr bool can_represent_native = string_like_is_target; + static constexpr bool can_partially_represent_native = can_represent_native; + static constexpr auto type = node_type::string; + }; + // string value_traits specializations - char8_t-based strings #if TOML_HAS_CHAR8 template <> diff --git a/include/toml++/impl/value.hpp b/include/toml++/impl/value.hpp index b6bcc5d7..3d18e5f9 100644 --- a/include/toml++/impl/value.hpp +++ b/include/toml++/impl/value.hpp @@ -44,7 +44,11 @@ TOML_DISABLE_ARITHMETIC_WARNINGS; TOML_SA_VALUE_MESSAGE_U8STRING_VIEW \ TOML_SA_LIST_SEP "const char*" \ TOML_SA_VALUE_MESSAGE_CONST_CHAR8 \ - TOML_SA_LIST_END + TOML_SA_LIST_END \ + \ + TOML_SA_LIST_NXT "A user-defined string type opted-in via toml::string_like" \ + TOML_SA_LIST_BEG "must be constructible from std::string_view" \ + TOML_SA_LIST_END #define TOML_SA_VALUE_FUNC_MESSAGE(type_arg) \ "The " type_arg " must be one of:" \ @@ -68,6 +72,10 @@ TOML_DISABLE_ARITHMETIC_WARNINGS; TOML_SA_VALUE_MESSAGE_U8STRING_VIEW \ TOML_SA_LIST_SEP "const char*" \ TOML_SA_VALUE_MESSAGE_CONST_CHAR8 \ + TOML_SA_LIST_END \ + \ + TOML_SA_LIST_NXT "A user-defined string type opted-in via toml::string_like" \ + TOML_SA_LIST_BEG "must be constructible from std::string_view" \ TOML_SA_LIST_END // clang-format on @@ -1018,6 +1026,16 @@ TOML_NAMESPACE_START #endif } + // char -> user-defined string type (see toml::string_like) + else if constexpr (string_like_is_target) + { + // prefer the view; it never allocates a temporary + if constexpr (std::is_constructible_v) + return T{ std::string_view{ str } }; + else + return T{ str }; + } + #if TOML_HAS_CHAR8 // char -> char8_t (potentially unsafe - the feature is 'experimental'!) @@ -1244,6 +1262,10 @@ TOML_NAMESPACE_START TOML_SA_LIST_SEP "const wchar_t*" #endif TOML_SA_LIST_END + + TOML_SA_LIST_NXT "A user-defined string type opted-in via toml::string_like" + TOML_SA_LIST_BEG "must be constructible from std::string_view" + TOML_SA_LIST_END ); // clang-format on diff --git a/src/modules/tomlplusplus.cppm b/src/modules/tomlplusplus.cppm index e1860c48..b43a3d94 100644 --- a/src/modules/tomlplusplus.cppm +++ b/src/modules/tomlplusplus.cppm @@ -38,6 +38,7 @@ export namespace toml { using TOML_NAMESPACE::path_component; using TOML_NAMESPACE::source_position; using TOML_NAMESPACE::source_region; + using TOML_NAMESPACE::string_like; using TOML_NAMESPACE::table; using TOML_NAMESPACE::time; using TOML_NAMESPACE::time_offset; diff --git a/tests/custom_string.cpp b/tests/custom_string.cpp new file mode 100644 index 00000000..4bd5b94d --- /dev/null +++ b/tests/custom_string.cpp @@ -0,0 +1,338 @@ +// This file is a part of toml++ and is subject to the the terms of the MIT license. +// Copyright (c) Mark Gillard +// See https://github.com/marzer/tomlplusplus/blob/master/LICENSE for the full license text. +// SPDX-License-Identifier: MIT + +#include "tests.hpp" + +namespace +{ + // a user-defined string type supporting both directions + struct udstring + { + std::string value; + + explicit udstring(std::string_view sv) // + : value{ sv } + {} + + /*implicit*/ operator std::string_view() const noexcept + { + return std::string_view{ value }; + } + + TOML_NODISCARD + friend bool operator==(const udstring& lhs, std::string_view rhs) noexcept + { + return lhs.value == rhs; + } + }; + + // ... usable as an initializer only: convertible to a view, but constructible from + // neither std::string_view nor std::string + struct udstring_in_only + { + std::string value; + + explicit udstring_in_only(const char* v) // + : value{ v } + {} + + /*implicit*/ operator std::string_view() const noexcept + { + return std::string_view{ value }; + } + }; + + // ... usable as a retrieval target only (no conversion to string_view) + struct udstring_out_only + { + std::string value; + + explicit udstring_out_only(std::string_view sv) // + : value{ sv } + {} + }; + + // ... converts via std::string rather than std::string_view in *both* directions. + // note this type is deliberately NOT convertible to/from std::string_view: a chain through + // operator std::string() would need two user-defined conversions, which is ill-formed. + struct udstring_via_string + { + std::string value; + + explicit udstring_via_string(std::string v) // + : value{ std::move(v) } + {} + + /*implicit*/ operator std::string() const + { + return value; + } + }; + + // ... convertible both ways, but deliberately NOT opted in + struct udstring_unregistered + { + std::string value; + + explicit udstring_unregistered(std::string_view sv) // + : value{ sv } + {} + + /*implicit*/ operator std::string_view() const noexcept + { + return std::string_view{ value }; + } + }; + + // ... opted in, and nothrow-constructible from a view + struct udstring_view + { + std::string_view value; + + /*implicit*/ udstring_view(std::string_view sv) noexcept // + : value{ sv } + {} + + /*implicit*/ operator std::string_view() const noexcept + { + return value; + } + }; +} + +namespace toml +{ + template <> + struct string_like : std::true_type + {}; + template <> + struct string_like : std::true_type + {}; + template <> + struct string_like : std::true_type + {}; + template <> + struct string_like : std::true_type + {}; + template <> + struct string_like : std::true_type + {}; +} + +TEST_CASE("custom string types - traits") +{ + using impl::value_traits; + + // opting in is necessary... + static_assert(!toml::string_like::value); + static_assert(value_traits::type == node_type::none); + static_assert(!value_traits::is_losslessly_convertible_to_native); + static_assert(!value_traits::can_represent_native); + static_assert(std::is_same_v, void>); + + // ... and the built-in types are untouched by any of this + static_assert(value_traits::is_native); + static_assert(value_traits::type == node_type::string); + static_assert(!value_traits::is_native); + static_assert(value_traits::type == node_type::integer); + static_assert(value_traits::type == node_type::floating_point); + static_assert(std::is_same_v, toml::value>); + + // registered types are strings, but never 'native' - storage stays std::string + static_assert(value_traits::type == node_type::string); + static_assert(!value_traits::is_native); + static_assert(std::is_same_v, std::string>); + static_assert(std::is_same_v, toml::value>); + + // ... so toml::is_string, which asks whether a type IS a string node, stays false for them + static_assert(!toml::is_string); + static_assert(toml::is_string); + + // the two directions are tracked independently + static_assert(value_traits::is_losslessly_convertible_to_native); // in + static_assert(value_traits::can_represent_native); // out + + static_assert(!std::is_constructible_v); + static_assert(!std::is_constructible_v); + static_assert(value_traits::is_losslessly_convertible_to_native); + static_assert(!value_traits::can_represent_native); + static_assert(!value_traits::can_partially_represent_native); + + static_assert(!value_traits::is_losslessly_convertible_to_native); + static_assert(value_traits::can_represent_native); + + // cvref-qualified forms resolve the same way + static_assert(value_traits::type == node_type::string); + static_assert(value_traits::can_represent_native); + + // a type that converts via std::string rather than std::string_view is supported in both directions, + // even though it is convertible to neither a view nor constructible from one + static_assert(!std::is_convertible_v); + static_assert(!std::is_constructible_v); + static_assert(std::is_convertible_v); + static_assert(std::is_constructible_v); + + static_assert(value_traits::type == node_type::string); + static_assert(value_traits::is_losslessly_convertible_to_native); + static_assert(value_traits::can_represent_native); +} + +TEST_CASE("custom string types - noexcept propagation") +{ + // udstring allocates, so retrieving one can throw + static_assert(!impl::value_retrieval_is_nothrow); + static_assert(!noexcept(std::declval().value())); + + // udstring_view does not + static_assert(impl::value_retrieval_is_nothrow); + static_assert(noexcept(std::declval().value())); + + // unchanged for the built-ins + static_assert(!impl::value_retrieval_is_nothrow); + static_assert(impl::value_retrieval_is_nothrow); + static_assert(impl::value_retrieval_is_nothrow); + static_assert(impl::value_retrieval_is_nothrow); + static_assert(impl::value_retrieval_is_nothrow); +} + +TEST_CASE("custom string types - as initializers") +{ + // direct construction of a value + { + auto v = toml::value{ udstring{ "hello"sv } }; + static_assert(std::is_same_v>); + CHECK(*v == "hello"sv); + } + + // insertion into a table + { + toml::table tbl; + tbl.insert("a", udstring{ "kek"sv }); + tbl.insert_or_assign("b", udstring_in_only{ "foo" }); + tbl.emplace("c", udstring{ "bar"sv }); + + REQUIRE(tbl["a"].is_string()); + CHECK(tbl["a"] == "kek"sv); + CHECK(tbl["b"] == "foo"sv); + CHECK(tbl["c"] == "bar"sv); + } + + // pushing into an array + { + toml::array arr; + arr.push_back(udstring{ "one"sv }); + arr.emplace_back(udstring{ "two"sv }); + + REQUIRE(arr.size() == 2u); + REQUIRE(arr[0].as_string() != nullptr); + REQUIRE(arr[1].as_string() != nullptr); + CHECK(arr[0].as_string()->get() == "one"sv); + CHECK(arr[1].as_string()->get() == "two"sv); + } + + // overwriting an existing key + { + toml::table tbl; + tbl.insert("x", "original"sv); + tbl.insert_or_assign("x", udstring{ "replaced"sv }); + CHECK(tbl["x"] == "replaced"sv); + } + + // assigning to an existing value + { + toml::value v{ "before"sv }; + v = udstring{ "after"sv }; + CHECK(*v == "after"sv); + } + + // a non-owning view type works as an initializer too (the stored value is a copy) + { + static_assert(impl::string_like_is_initializer); + + toml::table tbl; + { + const std::string owner{ "borrowed" }; + tbl.insert("v", udstring_view{ owner }); + } + CHECK(tbl["v"] == "borrowed"sv); + } + + // a type that has *not* opted in is still usable, just not implicitly - the caller + // converts at the call site, which is exactly the status quo this feature removes + { + const udstring_unregistered u{ "manual"sv }; + + toml::table tbl; + tbl.insert("k", std::string_view{ u }); + CHECK(tbl["k"] == "manual"sv); + } +} + +TEST_CASE("custom string types - as retrieval targets") +{ + static constexpr auto toml_text = R"( + name = "toml++" + count = 42 + )"sv; + + auto res = toml::parse(toml_text); +#if !TOML_EXCEPTIONS + REQUIRE(res.succeeded()); +#endif + table& tbl = res; + + // node::value() + { + const auto val = tbl["name"].value(); + REQUIRE(val.has_value()); + CHECK(*val == "toml++"sv); + } + + // node::value_exact() + { + const auto val = tbl["name"].value_exact(); + REQUIRE(val.has_value()); + CHECK(*val == "toml++"sv); + } + + // node::value_or() + { + CHECK(tbl["name"].value_or(udstring{ "fallback"sv }) == "toml++"sv); + CHECK(tbl["nope"].value_or(udstring{ "fallback"sv }) == "fallback"sv); + } + + // wrong node type yields nullopt, same as for std::string + { + CHECK(!tbl["count"].value().has_value()); + CHECK(!tbl["count"].value_exact().has_value()); + CHECK(tbl["count"].value_or(udstring{ "fallback"sv }) == "fallback"sv); + } + + // a retrieval-target-only type + { + const auto val = tbl["name"].value(); + REQUIRE(val.has_value()); + CHECK(val->value == "toml++"sv); + } + + // a non-allocating view type sees the stored string + { + const auto val = tbl["name"].value(); + REQUIRE(val.has_value()); + CHECK(val->value == "toml++"sv); + CHECK(val->value.data() == tbl["name"].as_string()->get().data()); + } + + // a type that only converts via std::string, in both directions + { + const auto val = tbl["name"].value(); + REQUIRE(val.has_value()); + CHECK(val->value == "toml++"sv); + + toml::table out; + out.insert("k", udstring_via_string{ "round-tripped" }); + CHECK(out["k"] == "round-tripped"sv); + } +} diff --git a/tests/meson.build b/tests/meson.build index 41329788..8ad79071 100644 --- a/tests/meson.build +++ b/tests/meson.build @@ -11,6 +11,7 @@ test_sources = files( 'conformance_burntsushi_valid.cpp', 'conformance_iarna_invalid.cpp', 'conformance_iarna_valid.cpp', + 'custom_string.cpp', 'formatters.cpp', 'for_each.cpp', 'impl_toml.cpp', diff --git a/tests/vs/test_debug_x64.vcxproj b/tests/vs/test_debug_x64.vcxproj index 63573b02..1444eeff 100644 --- a/tests/vs/test_debug_x64.vcxproj +++ b/tests/vs/test_debug_x64.vcxproj @@ -79,6 +79,7 @@ + diff --git a/tests/vs/test_debug_x64_cpplatest.vcxproj b/tests/vs/test_debug_x64_cpplatest.vcxproj index 9580b532..7ce1363d 100644 --- a/tests/vs/test_debug_x64_cpplatest.vcxproj +++ b/tests/vs/test_debug_x64_cpplatest.vcxproj @@ -79,6 +79,7 @@ + diff --git a/tests/vs/test_debug_x64_cpplatest_noexcept.vcxproj b/tests/vs/test_debug_x64_cpplatest_noexcept.vcxproj index 786eb737..9fa62223 100644 --- a/tests/vs/test_debug_x64_cpplatest_noexcept.vcxproj +++ b/tests/vs/test_debug_x64_cpplatest_noexcept.vcxproj @@ -79,6 +79,7 @@ + diff --git a/tests/vs/test_debug_x64_cpplatest_noexcept_unrel.vcxproj b/tests/vs/test_debug_x64_cpplatest_noexcept_unrel.vcxproj index eb327914..3317e7e7 100644 --- a/tests/vs/test_debug_x64_cpplatest_noexcept_unrel.vcxproj +++ b/tests/vs/test_debug_x64_cpplatest_noexcept_unrel.vcxproj @@ -79,6 +79,7 @@ + diff --git a/tests/vs/test_debug_x64_cpplatest_unrel.vcxproj b/tests/vs/test_debug_x64_cpplatest_unrel.vcxproj index a1e8fd72..059a2653 100644 --- a/tests/vs/test_debug_x64_cpplatest_unrel.vcxproj +++ b/tests/vs/test_debug_x64_cpplatest_unrel.vcxproj @@ -79,6 +79,7 @@ + diff --git a/tests/vs/test_debug_x64_noexcept.vcxproj b/tests/vs/test_debug_x64_noexcept.vcxproj index 24e110d1..4fc56bc3 100644 --- a/tests/vs/test_debug_x64_noexcept.vcxproj +++ b/tests/vs/test_debug_x64_noexcept.vcxproj @@ -79,6 +79,7 @@ + diff --git a/tests/vs/test_debug_x64_noexcept_unrel.vcxproj b/tests/vs/test_debug_x64_noexcept_unrel.vcxproj index 5ae2df3d..aafe17b7 100644 --- a/tests/vs/test_debug_x64_noexcept_unrel.vcxproj +++ b/tests/vs/test_debug_x64_noexcept_unrel.vcxproj @@ -79,6 +79,7 @@ + diff --git a/tests/vs/test_debug_x64_unrel.vcxproj b/tests/vs/test_debug_x64_unrel.vcxproj index 50335835..c0bb4816 100644 --- a/tests/vs/test_debug_x64_unrel.vcxproj +++ b/tests/vs/test_debug_x64_unrel.vcxproj @@ -79,6 +79,7 @@ + diff --git a/tests/vs/test_debug_x86.vcxproj b/tests/vs/test_debug_x86.vcxproj index b6eb5c4f..fca817a9 100644 --- a/tests/vs/test_debug_x86.vcxproj +++ b/tests/vs/test_debug_x86.vcxproj @@ -79,6 +79,7 @@ + diff --git a/tests/vs/test_debug_x86_cpplatest.vcxproj b/tests/vs/test_debug_x86_cpplatest.vcxproj index 72506f22..1d9b3c59 100644 --- a/tests/vs/test_debug_x86_cpplatest.vcxproj +++ b/tests/vs/test_debug_x86_cpplatest.vcxproj @@ -79,6 +79,7 @@ + diff --git a/tests/vs/test_debug_x86_cpplatest_noexcept.vcxproj b/tests/vs/test_debug_x86_cpplatest_noexcept.vcxproj index d3dfa361..0c0ab069 100644 --- a/tests/vs/test_debug_x86_cpplatest_noexcept.vcxproj +++ b/tests/vs/test_debug_x86_cpplatest_noexcept.vcxproj @@ -79,6 +79,7 @@ + diff --git a/tests/vs/test_debug_x86_cpplatest_noexcept_unrel.vcxproj b/tests/vs/test_debug_x86_cpplatest_noexcept_unrel.vcxproj index 4a433def..1290629c 100644 --- a/tests/vs/test_debug_x86_cpplatest_noexcept_unrel.vcxproj +++ b/tests/vs/test_debug_x86_cpplatest_noexcept_unrel.vcxproj @@ -79,6 +79,7 @@ + diff --git a/tests/vs/test_debug_x86_cpplatest_unrel.vcxproj b/tests/vs/test_debug_x86_cpplatest_unrel.vcxproj index 69839eca..0b0656e0 100644 --- a/tests/vs/test_debug_x86_cpplatest_unrel.vcxproj +++ b/tests/vs/test_debug_x86_cpplatest_unrel.vcxproj @@ -79,6 +79,7 @@ + diff --git a/tests/vs/test_debug_x86_noexcept.vcxproj b/tests/vs/test_debug_x86_noexcept.vcxproj index 1d7098ba..2e4ee717 100644 --- a/tests/vs/test_debug_x86_noexcept.vcxproj +++ b/tests/vs/test_debug_x86_noexcept.vcxproj @@ -79,6 +79,7 @@ + diff --git a/tests/vs/test_debug_x86_noexcept_unrel.vcxproj b/tests/vs/test_debug_x86_noexcept_unrel.vcxproj index 7f4fe51a..977869ef 100644 --- a/tests/vs/test_debug_x86_noexcept_unrel.vcxproj +++ b/tests/vs/test_debug_x86_noexcept_unrel.vcxproj @@ -79,6 +79,7 @@ + diff --git a/tests/vs/test_debug_x86_unrel.vcxproj b/tests/vs/test_debug_x86_unrel.vcxproj index 62edabce..17490416 100644 --- a/tests/vs/test_debug_x86_unrel.vcxproj +++ b/tests/vs/test_debug_x86_unrel.vcxproj @@ -79,6 +79,7 @@ + diff --git a/tests/vs/test_release_x64.vcxproj b/tests/vs/test_release_x64.vcxproj index 8836ba04..cae8283f 100644 --- a/tests/vs/test_release_x64.vcxproj +++ b/tests/vs/test_release_x64.vcxproj @@ -79,6 +79,7 @@ + diff --git a/tests/vs/test_release_x64_cpplatest.vcxproj b/tests/vs/test_release_x64_cpplatest.vcxproj index 7e5cbd06..489a786b 100644 --- a/tests/vs/test_release_x64_cpplatest.vcxproj +++ b/tests/vs/test_release_x64_cpplatest.vcxproj @@ -79,6 +79,7 @@ + diff --git a/tests/vs/test_release_x64_cpplatest_noexcept.vcxproj b/tests/vs/test_release_x64_cpplatest_noexcept.vcxproj index cff40b6c..6440242f 100644 --- a/tests/vs/test_release_x64_cpplatest_noexcept.vcxproj +++ b/tests/vs/test_release_x64_cpplatest_noexcept.vcxproj @@ -79,6 +79,7 @@ + diff --git a/tests/vs/test_release_x64_cpplatest_noexcept_unrel.vcxproj b/tests/vs/test_release_x64_cpplatest_noexcept_unrel.vcxproj index 6e260d2a..ac96c7be 100644 --- a/tests/vs/test_release_x64_cpplatest_noexcept_unrel.vcxproj +++ b/tests/vs/test_release_x64_cpplatest_noexcept_unrel.vcxproj @@ -79,6 +79,7 @@ + diff --git a/tests/vs/test_release_x64_cpplatest_unrel.vcxproj b/tests/vs/test_release_x64_cpplatest_unrel.vcxproj index ee9312c2..7bcb1f7f 100644 --- a/tests/vs/test_release_x64_cpplatest_unrel.vcxproj +++ b/tests/vs/test_release_x64_cpplatest_unrel.vcxproj @@ -79,6 +79,7 @@ + diff --git a/tests/vs/test_release_x64_noexcept.vcxproj b/tests/vs/test_release_x64_noexcept.vcxproj index 6247285b..5127adf9 100644 --- a/tests/vs/test_release_x64_noexcept.vcxproj +++ b/tests/vs/test_release_x64_noexcept.vcxproj @@ -79,6 +79,7 @@ + diff --git a/tests/vs/test_release_x64_noexcept_unrel.vcxproj b/tests/vs/test_release_x64_noexcept_unrel.vcxproj index 024f112e..b2d311f3 100644 --- a/tests/vs/test_release_x64_noexcept_unrel.vcxproj +++ b/tests/vs/test_release_x64_noexcept_unrel.vcxproj @@ -79,6 +79,7 @@ + diff --git a/tests/vs/test_release_x64_unrel.vcxproj b/tests/vs/test_release_x64_unrel.vcxproj index 629c7ed0..7051408e 100644 --- a/tests/vs/test_release_x64_unrel.vcxproj +++ b/tests/vs/test_release_x64_unrel.vcxproj @@ -79,6 +79,7 @@ + diff --git a/tests/vs/test_release_x86.vcxproj b/tests/vs/test_release_x86.vcxproj index 1d23707a..f5e1818c 100644 --- a/tests/vs/test_release_x86.vcxproj +++ b/tests/vs/test_release_x86.vcxproj @@ -79,6 +79,7 @@ + diff --git a/tests/vs/test_release_x86_cpplatest.vcxproj b/tests/vs/test_release_x86_cpplatest.vcxproj index 8790e2d6..8d0db180 100644 --- a/tests/vs/test_release_x86_cpplatest.vcxproj +++ b/tests/vs/test_release_x86_cpplatest.vcxproj @@ -79,6 +79,7 @@ + diff --git a/tests/vs/test_release_x86_cpplatest_noexcept.vcxproj b/tests/vs/test_release_x86_cpplatest_noexcept.vcxproj index b001d2f5..e542ca8d 100644 --- a/tests/vs/test_release_x86_cpplatest_noexcept.vcxproj +++ b/tests/vs/test_release_x86_cpplatest_noexcept.vcxproj @@ -79,6 +79,7 @@ + diff --git a/tests/vs/test_release_x86_cpplatest_noexcept_unrel.vcxproj b/tests/vs/test_release_x86_cpplatest_noexcept_unrel.vcxproj index 93dfac1c..6b65294e 100644 --- a/tests/vs/test_release_x86_cpplatest_noexcept_unrel.vcxproj +++ b/tests/vs/test_release_x86_cpplatest_noexcept_unrel.vcxproj @@ -79,6 +79,7 @@ + diff --git a/tests/vs/test_release_x86_cpplatest_unrel.vcxproj b/tests/vs/test_release_x86_cpplatest_unrel.vcxproj index fb18b9f7..9281a4b7 100644 --- a/tests/vs/test_release_x86_cpplatest_unrel.vcxproj +++ b/tests/vs/test_release_x86_cpplatest_unrel.vcxproj @@ -79,6 +79,7 @@ + diff --git a/tests/vs/test_release_x86_noexcept.vcxproj b/tests/vs/test_release_x86_noexcept.vcxproj index 291c77fd..aa9c4230 100644 --- a/tests/vs/test_release_x86_noexcept.vcxproj +++ b/tests/vs/test_release_x86_noexcept.vcxproj @@ -79,6 +79,7 @@ + diff --git a/tests/vs/test_release_x86_noexcept_unrel.vcxproj b/tests/vs/test_release_x86_noexcept_unrel.vcxproj index 39ce1308..96a982c0 100644 --- a/tests/vs/test_release_x86_noexcept_unrel.vcxproj +++ b/tests/vs/test_release_x86_noexcept_unrel.vcxproj @@ -79,6 +79,7 @@ + diff --git a/tests/vs/test_release_x86_unrel.vcxproj b/tests/vs/test_release_x86_unrel.vcxproj index cbdedd38..6e5115b4 100644 --- a/tests/vs/test_release_x86_unrel.vcxproj +++ b/tests/vs/test_release_x86_unrel.vcxproj @@ -79,6 +79,7 @@ + diff --git a/toml.hpp b/toml.hpp index cb738507..263967eb 100644 --- a/toml.hpp +++ b/toml.hpp @@ -1692,6 +1692,10 @@ TOML_NAMESPACE_START // abi namespace inserter(T&) -> inserter; using default_formatter = toml_formatter; + + template + struct string_like : std::false_type + {}; } TOML_NAMESPACE_END; @@ -1716,12 +1720,42 @@ TOML_IMPL_NAMESPACE_START inline constexpr bool is_wide_string = is_one_of, const wchar_t*, wchar_t*, std::wstring_view, std::wstring>; + // user-defined string types (see toml::string_like) + + template + inline constexpr bool is_string_like = toml::string_like>::value; + + // ... usable as an initializer for a TOML string value. + // note that a type converting via operator std::string() is *not* convertible to std::string_view (that would + // require two user-defined conversions), so both targets have to be checked independently. + template + inline constexpr bool string_like_is_initializer = + is_string_like + && (std::is_convertible_v&, std::string_view> + || std::is_convertible_v&, std::string>); + + // ... usable as a target type when retrieving a TOML string value + template + inline constexpr bool string_like_is_target = is_string_like + && (std::is_constructible_v, std::string_view> + || std::is_constructible_v, const std::string&>); + + // ... and whether constructing that target can throw (it is built from whichever of the two the type supports, + // preferring the view since it never allocates a temporary) + template + inline constexpr bool string_like_target_is_nothrow = + std::is_constructible_v, std::string_view> + ? std::is_nothrow_constructible_v, std::string_view> + : std::is_nothrow_constructible_v, const std::string&>; + template inline constexpr bool value_retrieval_is_nothrow = !std::is_same_v, std::string> #if TOML_HAS_CHAR8 && !std::is_same_v, std::u8string> #endif - + // user-defined string types are constructed from the stored + // std::string, so they can throw iff that construction can + && !(string_like_is_target && !string_like_target_is_nothrow) && !is_wide_string; template @@ -1816,12 +1850,17 @@ TOML_IMPL_NAMESPACE_START template struct value_traits; + // (defined alongside the other string traits, below) + template + struct user_string_traits; + + // note: enums cannot have conversion operators, so the enum and string-like cases are mutually exclusive template > struct value_traits_base_selector { static_assert(!is_cvref); - using type = default_value_traits; + using type = std::conditional_t, user_string_traits, default_value_traits>; }; template struct value_traits_base_selector @@ -2051,6 +2090,23 @@ TOML_IMPL_NAMESPACE_START struct value_traits : string_traits {}; + // string value_traits specializations - user-defined string types (see toml::string_like) + // + // unlike the built-in string types these are never 'native' (storage is always std::string), and the two + // directions are tracked independently, since a type may support only one of them: + // - is_losslessly_convertible_to_native => usable as an initializer (T -> std::string_view) + // - can_represent_native => usable as a retrieval target (std::string_view -> T) + template + struct user_string_traits + { + using native_type = std::string; + static constexpr bool is_native = false; + static constexpr bool is_losslessly_convertible_to_native = string_like_is_initializer; + static constexpr bool can_represent_native = string_like_is_target; + static constexpr bool can_partially_represent_native = can_represent_native; + static constexpr auto type = node_type::string; + }; + // string value_traits specializations - char8_t-based strings #if TOML_HAS_CHAR8 template <> @@ -4904,7 +4960,11 @@ TOML_DISABLE_ARITHMETIC_WARNINGS; TOML_SA_VALUE_MESSAGE_U8STRING_VIEW \ TOML_SA_LIST_SEP "const char*" \ TOML_SA_VALUE_MESSAGE_CONST_CHAR8 \ - TOML_SA_LIST_END + TOML_SA_LIST_END \ + \ + TOML_SA_LIST_NXT "A user-defined string type opted-in via toml::string_like" \ + TOML_SA_LIST_BEG "must be constructible from std::string_view" \ + TOML_SA_LIST_END #define TOML_SA_VALUE_FUNC_MESSAGE(type_arg) \ "The " type_arg " must be one of:" \ @@ -4928,6 +4988,10 @@ TOML_DISABLE_ARITHMETIC_WARNINGS; TOML_SA_VALUE_MESSAGE_U8STRING_VIEW \ TOML_SA_LIST_SEP "const char*" \ TOML_SA_VALUE_MESSAGE_CONST_CHAR8 \ + TOML_SA_LIST_END \ + \ + TOML_SA_LIST_NXT "A user-defined string type opted-in via toml::string_like" \ + TOML_SA_LIST_BEG "must be constructible from std::string_view" \ TOML_SA_LIST_END // clang-format on @@ -5694,6 +5758,16 @@ TOML_NAMESPACE_START #endif } + // char -> user-defined string type (see toml::string_like) + else if constexpr (string_like_is_target) + { + // prefer the view; it never allocates a temporary + if constexpr (std::is_constructible_v) + return T{ std::string_view{ str } }; + else + return T{ str }; + } + #if TOML_HAS_CHAR8 // char -> char8_t (potentially unsafe - the feature is 'experimental'!) @@ -5920,6 +5994,10 @@ TOML_NAMESPACE_START TOML_SA_LIST_SEP "const wchar_t*" #endif TOML_SA_LIST_END + + TOML_SA_LIST_NXT "A user-defined string type opted-in via toml::string_like" + TOML_SA_LIST_BEG "must be constructible from std::string_view" + TOML_SA_LIST_END ); // clang-format on diff --git a/tools/generate_windows_test_targets.py b/tools/generate_windows_test_targets.py index 45221eaa..93690500 100755 --- a/tools/generate_windows_test_targets.py +++ b/tools/generate_windows_test_targets.py @@ -122,6 +122,7 @@ def main(): +