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..534fdf59 100644 --- a/include/toml++/impl/forward_declarations.hpp +++ b/include/toml++/impl/forward_declarations.hpp @@ -407,13 +407,89 @@ 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-like' types (see toml::is_string_like), detected via expression SFINAE as any type with: + // - a const-qualified data() or c_str() member returning (something convertible to) const char*, and + // - a const-qualified length() member returning (something convertible to) size_t. + + template + struct has_string_like_data : std::false_type + {}; + template + struct has_string_like_data< + T, + std::enable_if_t().data()), const char*>>> + : std::true_type + {}; + + template + struct has_string_like_c_str : std::false_type + {}; + template + struct has_string_like_c_str< + T, + std::enable_if_t().c_str()), const char*>>> + : std::true_type + {}; + + template + struct has_string_like_length : std::false_type + {}; + template + struct has_string_like_length< + T, + std::enable_if_t().length()), size_t>>> : std::true_type + {}; + + // (std::string and std::string_view would satisfy the member checks but are first-class citizens + // with their own dedicated handling, so they're excluded from the user-defined detection) + template + inline constexpr bool is_string_like = // + std::is_class_v> // + && !is_one_of, std::string, std::string_view> // + && (has_string_like_data>::value // + || has_string_like_c_str>::value) // + && has_string_like_length>::value; + + // the view over a string-like's character data (preferring data() where both it and c_str() are present) + template + TOML_NODISCARD + inline std::string_view string_like_to_view(const T& str) + { + static_assert(is_string_like); + + if constexpr (has_string_like_data>::value) + return std::string_view{ static_cast(str.data()), static_cast(str.length()) }; + else + return std::string_view{ static_cast(str.c_str()), static_cast(str.length()) }; + } + + // ... usable as a target type when retrieving a TOML string value (storage is always std::string, + // so the type must be constructible from the stored string or a view/pointer into it) + template + inline constexpr bool string_like_is_target = is_string_like + && (std::is_constructible_v, std::string_view> + || std::is_constructible_v, const char*, size_t> + || std::is_constructible_v, const std::string&>); + + // ... and whether constructing that target can throw (it is built from the first of + // std::string_view / (const char*, size_t) / const std::string& that the type supports) template - inline constexpr bool value_retrieval_is_nothrow = !std::is_same_v, std::string> + 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_constructible_v, const char*, size_t> + ? std::is_nothrow_constructible_v, const char*, size_t> + : 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> + && !std::is_same_v, std::u8string> #endif - - && !is_wide_string; + // 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 struct copy_ref_; @@ -507,12 +583,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 member functions, 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 +823,24 @@ TOML_IMPL_NAMESPACE_START struct value_traits : string_traits {}; + // string value_traits specializations - user-defined string-like types (see toml::is_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 (always true - the detected + // data()/c_str() + length() members provide the character data) + // - can_represent_native => usable as a retrieval target (requires a suitable constructor) + template + struct user_string_traits + { + using native_type = std::string; + static constexpr bool is_native = false; + static constexpr bool is_losslessly_convertible_to_native = true; + 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 <> @@ -1028,6 +1127,30 @@ TOML_NAMESPACE_START /// \brief Metafunction for determining if a type is, or is a reference to, a toml::node_view. template inline constexpr bool is_node_view = impl::is_one_of, node_view, node_view>; + + /// \brief Metafunction for determining if a type is a user-defined 'string-like' type. + /// + /// \detail User-defined string types are detected automatically and may be used anywhere the library + /// expects a string - as an initializer for TOML string values, and as a target type for + /// node::value(), node::value_or() and friends: \cpp + /// tbl.insert("hostname", foo::String{ "localhost" }); + /// auto hostname = tbl["hostname"].value(); + /// \ecpp + /// + /// A type is detected as string-like when it has: + /// - a const-qualified `data()` or `c_str()` member returning (something convertible to) `const char*`, and + /// - a const-qualified `length()` member returning (something convertible to) `size_t`. + /// + /// Being detected is sufficient to use a type as an initializer; to also be usable as a + /// retrieval target it must additionally be constructible from one of `std::string_view`, + /// `(const char*, size_t)` or `const std::string&`. Attempting to retrieve a value as a + /// string-like type without a suitable constructor is a compile error. + /// + /// \note TOML string values are always stored internally as std::string; detection 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 + inline constexpr bool is_string_like = impl::is_string_like; } TOML_NAMESPACE_END; diff --git a/include/toml++/impl/value.hpp b/include/toml++/impl/value.hpp index b6bcc5d7..4931c193 100644 --- a/include/toml++/impl/value.hpp +++ b/include/toml++/impl/value.hpp @@ -44,7 +44,13 @@ 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-like' type (see toml::is_string_like)" \ + TOML_SA_LIST_BEG "with const char* data() const or const char* c_str() const" \ + TOML_SA_LIST_SEP "and size_t length() const" \ + TOML_SA_LIST_SEP "and constructible from std::string_view, (const char*, size_t) or const std::string&" \ + TOML_SA_LIST_END #define TOML_SA_VALUE_FUNC_MESSAGE(type_arg) \ "The " type_arg " must be one of:" \ @@ -68,6 +74,12 @@ 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-like' type (see toml::is_string_like)" \ + TOML_SA_LIST_BEG "with const char* data() const or const char* c_str() const" \ + TOML_SA_LIST_SEP "and size_t length() const" \ + TOML_SA_LIST_SEP "and constructible from std::string_view, (const char*, size_t) or const std::string&" \ TOML_SA_LIST_END // clang-format on @@ -83,7 +95,11 @@ TOML_IMPL_NAMESPACE_START TOML_NODISCARD static T make(Args&&... args) noexcept(std::is_nothrow_constructible_v) { - if constexpr (std::is_aggregate_v) + // user-defined string-likes (see toml::is_string_like) provide their character data + // via data()/c_str() + length() rather than by conversion + if constexpr (std::is_same_v && sizeof...(Args) == 1u && (is_string_like && ...)) + return std::string{ string_like_to_view(args)... }; + else if constexpr (std::is_aggregate_v) return T{ static_cast(args)... }; else return T(static_cast(args)...); @@ -1018,6 +1034,19 @@ TOML_NAMESPACE_START #endif } + // char -> user-defined string-like type (see toml::is_string_like) + else if constexpr (string_like_is_target) + { + // construction preference order mirrors string_like_target_is_nothrow + // (parenthesized init, matching the std::is_constructible checks) + if constexpr (std::is_constructible_v) + return T(std::string_view{ str }); + else if constexpr (std::is_constructible_v) + return T(str.data(), str.length()); + else + return T(str); + } + #if TOML_HAS_CHAR8 // char -> char8_t (potentially unsafe - the feature is 'experimental'!) @@ -1244,6 +1273,12 @@ TOML_NAMESPACE_START TOML_SA_LIST_SEP "const wchar_t*" #endif TOML_SA_LIST_END + + TOML_SA_LIST_NXT "A user-defined 'string-like' type (see toml::is_string_like)" + TOML_SA_LIST_BEG "with const char* data() const or const char* c_str() const" + TOML_SA_LIST_SEP "and size_t length() const" + TOML_SA_LIST_SEP "and constructible from std::string_view, (const char*, size_t) or const std::string&" + TOML_SA_LIST_END ); // clang-format on diff --git a/src/modules/tomlplusplus.cppm b/src/modules/tomlplusplus.cppm index e1860c48..1a8e3fd9 100644 --- a/src/modules/tomlplusplus.cppm +++ b/src/modules/tomlplusplus.cppm @@ -80,6 +80,7 @@ export namespace toml { using TOML_NAMESPACE::is_node_view; using TOML_NAMESPACE::is_number; using TOML_NAMESPACE::is_string; + using TOML_NAMESPACE::is_string_like; using TOML_NAMESPACE::is_table; using TOML_NAMESPACE::is_time; using TOML_NAMESPACE::is_value; diff --git a/tests/custom_string.cpp b/tests/custom_string.cpp new file mode 100644 index 00000000..59238b9c --- /dev/null +++ b/tests/custom_string.cpp @@ -0,0 +1,403 @@ +// 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" + +#include + +namespace +{ + // a user-defined string type detected via its data() + length() members. + // deliberately NOT convertible to std::string_view - detection needs no conversions. + struct UserDefinedString + { + std::string value; + + explicit UserDefinedString(std::string_view sv) // + : value{ sv } + {} + + const char* data() const noexcept + { + return value.data(); + } + + size_t length() const noexcept + { + return value.length(); + } + + TOML_NODISCARD + friend bool operator==(const UserDefinedString& lhs, std::string_view rhs) noexcept + { + return lhs.value == rhs; + } + }; + + // ... detected via c_str() + length() (no data()), and only constructible from (const char*, size_t) + struct UserDefinedCString + { + std::string value; + + UserDefinedCString(const char* str, size_t len) // + : value{ str, len } + {} + + const char* c_str() const noexcept + { + return value.c_str(); + } + + size_t length() const noexcept + { + return value.length(); + } + }; + + // ... only constructible from const std::string& + struct UserDefinedStringFromString + { + std::string value; + + explicit UserDefinedStringFromString(const std::string& str) // + : value{ str } + {} + + const char* data() const noexcept + { + return value.data(); + } + + size_t length() const noexcept + { + return value.length(); + } + }; + + // ... usable as an initializer only: detected, but with no constructor suitable for retrieval + struct UserDefinedStringInOnly + { + std::string value; + + explicit UserDefinedStringInOnly(const char* v) // + : value{ v } + {} + + const char* data() const noexcept + { + return value.data(); + } + + size_t length() const noexcept + { + return value.length(); + } + }; + + // ... a non-owning view type, nothrow-constructible from a view + struct UserDefinedStringView + { + std::string_view value; + + /*implicit*/ UserDefinedStringView(std::string_view sv) noexcept // + : value{ sv } + {} + + const char* data() const noexcept + { + return value.data(); + } + + size_t length() const noexcept + { + return value.length(); + } + }; + + // ... deriving from std::string inherits everything needed for detection. + // (types implicitly convertible to std::string_view are deliberately not excluded - support is + // dispatched via traits rather than overloads, so the conversion introduces no ambiguity) + struct UserDefinedStringDerived : std::string + { + using std::string::string; + }; + + // NOT string-like: no length() (size() doesn't count) + struct NonStringNoLength + { + std::string value; + + const char* data() const noexcept + { + return value.data(); + } + + size_t size() const noexcept + { + return value.size(); + } + }; + + // the types below exist only to be probed by the detection traits; unevaluated SFINAE contexts + // never odr-use their members, so they are left undefined and marked [[maybe_unused]] to satisfy + // clang's -Wunneeded-member-function and -Wunused-member-function + + // NOT string-like: character data is not char-based + struct NonStringWrongChar + { + [[maybe_unused]] const unsigned char* data() const noexcept; + [[maybe_unused]] size_t length() const noexcept; + }; + + // NOT string-like: members are not const-qualified + struct NonStringNonConst + { + [[maybe_unused]] const char* data() noexcept; + [[maybe_unused]] size_t length() noexcept; + }; + + // NOT string-like: members are not public + class NonStringPrivate + { + public: + [[maybe_unused]] NonStringPrivate() noexcept = default; + + private: + [[maybe_unused]] const char* data() const noexcept; + [[maybe_unused]] size_t length() const noexcept; + }; +} + +TEST_CASE("custom string types - detection") +{ + using impl::value_traits; + + // a type is detected as string-like when it has data() or c_str(), and length() + static_assert(toml::is_string_like); + static_assert(toml::is_string_like); + static_assert(toml::is_string_like); + static_assert(toml::is_string_like); + static_assert(toml::is_string_like); + static_assert(toml::is_string_like); + + // ... no conversion to std::string_view required + static_assert(!std::is_convertible_v); + + // ... and types missing either member (or with the wrong shape) are not + static_assert(!toml::is_string_like); + static_assert(!toml::is_string_like); + static_assert(!toml::is_string_like); + static_assert(!toml::is_string_like); + static_assert(!toml::is_string_like>); + static_assert(value_traits::type == node_type::none); + static_assert(std::is_same_v, void>); + + // ... while the built-in types are untouched by any of this + static_assert(!toml::is_string_like); + static_assert(!toml::is_string_like); + 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>); + + // detected 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); + + // every detected type is usable as an initializer; being a retrieval target additionally + // requires a constructor taking std::string_view, (const char*, size_t) or const std::string& + static_assert(value_traits::is_losslessly_convertible_to_native); // in + static_assert(value_traits::can_represent_native); // out + + static_assert(value_traits::is_losslessly_convertible_to_native); + static_assert(!value_traits::can_represent_native); + static_assert(!value_traits::can_partially_represent_native); + + // ... and each of the three constructor forms is sufficient on its own + static_assert(std::is_constructible_v); + static_assert(!std::is_constructible_v); + static_assert(std::is_constructible_v); + static_assert(!std::is_constructible_v); + static_assert(!std::is_constructible_v); + static_assert(std::is_constructible_v); + static_assert(value_traits::can_represent_native); + static_assert(value_traits::can_represent_native); + + // cvref-qualified forms resolve the same way + static_assert(toml::is_string_like); + static_assert(value_traits::type == node_type::string); + static_assert(value_traits::can_represent_native); +} + +TEST_CASE("custom string types - noexcept propagation") +{ + // UserDefinedString allocates, so retrieving one can throw + static_assert(!impl::value_retrieval_is_nothrow); + static_assert(!noexcept(std::declval().value())); + + // UserDefinedStringView 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{ UserDefinedString{ "hello"sv } }; + static_assert(std::is_same_v>); + CHECK(*v == "hello"sv); + } + + // insertion into a table + { + toml::table tbl; + tbl.insert("a", UserDefinedString{ "kek"sv }); + tbl.insert_or_assign("b", UserDefinedStringInOnly{ "foo" }); + tbl.emplace("c", UserDefinedString{ "bar"sv }); + tbl.insert("d", UserDefinedCString{ "qux", 3u }); // character data provided by c_str() + tbl.insert("e", UserDefinedStringFromString{ "baz" }); + + REQUIRE(tbl["a"].is_string()); + CHECK(tbl["a"] == "kek"sv); + CHECK(tbl["b"] == "foo"sv); + CHECK(tbl["c"] == "bar"sv); + CHECK(tbl["d"] == "qux"sv); + CHECK(tbl["e"] == "baz"sv); + } + + // pushing into an array + { + toml::array arr; + arr.push_back(UserDefinedString{ "one"sv }); + arr.emplace_back(UserDefinedString{ "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", UserDefinedString{ "replaced"sv }); + CHECK(tbl["x"] == "replaced"sv); + } + + // a non-owning view type works as an initializer too (the stored value is a copy) + { + toml::table tbl; + { + const std::string owner{ "borrowed" }; + tbl.insert("v", UserDefinedStringView{ owner }); + } + CHECK(tbl["v"] == "borrowed"sv); + } + + // a type derived from std::string is detected like any other string-like + { + toml::table tbl; + tbl.insert("d", UserDefinedStringDerived{ "inherited" }); + CHECK(tbl["d"] == "inherited"sv); + } + + // a type that is *not* detected is still usable, just not implicitly - the caller + // converts at the call site, which is exactly the status quo this feature removes + { + const NonStringNoLength n{ "manual" }; + + toml::table tbl; + tbl.insert("k", std::string_view{ n.data(), n.size() }); + 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(UserDefinedString{ "fallback"sv }) == "toml++"sv); + CHECK(tbl["nope"].value_or(UserDefinedString{ "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(UserDefinedString{ "fallback"sv }) == "fallback"sv); + } + + // a type constructed via (const char*, size_t) + { + const auto val = tbl["name"].value(); + REQUIRE(val.has_value()); + CHECK(val->value == "toml++"sv); + } + + // a type constructed via const std::string& + { + 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 derived from std::string + { + const auto val = tbl["name"].value(); + REQUIRE(val.has_value()); + CHECK(*val == "toml++"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..cfca8091 100644 --- a/toml.hpp +++ b/toml.hpp @@ -1716,13 +1716,89 @@ 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-like' types (see toml::is_string_like), detected via expression SFINAE as any type with: + // - a const-qualified data() or c_str() member returning (something convertible to) const char*, and + // - a const-qualified length() member returning (something convertible to) size_t. + + template + struct has_string_like_data : std::false_type + {}; + template + struct has_string_like_data< + T, + std::enable_if_t().data()), const char*>>> + : std::true_type + {}; + + template + struct has_string_like_c_str : std::false_type + {}; + template + struct has_string_like_c_str< + T, + std::enable_if_t().c_str()), const char*>>> + : std::true_type + {}; + + template + struct has_string_like_length : std::false_type + {}; + template + struct has_string_like_length< + T, + std::enable_if_t().length()), size_t>>> : std::true_type + {}; + + // (std::string and std::string_view would satisfy the member checks but are first-class citizens + // with their own dedicated handling, so they're excluded from the user-defined detection) + template + inline constexpr bool is_string_like = // + std::is_class_v> // + && !is_one_of, std::string, std::string_view> // + && (has_string_like_data>::value // + || has_string_like_c_str>::value) // + && has_string_like_length>::value; + + // the view over a string-like's character data (preferring data() where both it and c_str() are present) + template + TOML_NODISCARD + inline std::string_view string_like_to_view(const T& str) + { + static_assert(is_string_like); + + if constexpr (has_string_like_data>::value) + return std::string_view{ static_cast(str.data()), static_cast(str.length()) }; + else + return std::string_view{ static_cast(str.c_str()), static_cast(str.length()) }; + } + + // ... usable as a target type when retrieving a TOML string value (storage is always std::string, + // so the type must be constructible from the stored string or a view/pointer into it) + template + inline constexpr bool string_like_is_target = is_string_like + && (std::is_constructible_v, std::string_view> + || std::is_constructible_v, const char*, size_t> + || std::is_constructible_v, const std::string&>); + + // ... and whether constructing that target can throw (it is built from the first of + // std::string_view / (const char*, size_t) / const std::string& that the type supports) + 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_constructible_v, const char*, size_t> + ? std::is_nothrow_constructible_v, const char*, size_t> + : std::is_nothrow_constructible_v, const std::string&>); + template - inline constexpr bool value_retrieval_is_nothrow = !std::is_same_v, std::string> + inline constexpr bool value_retrieval_is_nothrow = + !std::is_same_v, std::string> #if TOML_HAS_CHAR8 - && !std::is_same_v, std::u8string> + && !std::is_same_v, std::u8string> #endif - - && !is_wide_string; + // 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 struct copy_ref_; @@ -1816,12 +1892,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 member functions, 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 +2132,24 @@ TOML_IMPL_NAMESPACE_START struct value_traits : string_traits {}; + // string value_traits specializations - user-defined string-like types (see toml::is_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 (always true - the detected + // data()/c_str() + length() members provide the character data) + // - can_represent_native => usable as a retrieval target (requires a suitable constructor) + template + struct user_string_traits + { + using native_type = std::string; + static constexpr bool is_native = false; + static constexpr bool is_losslessly_convertible_to_native = true; + 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 <> @@ -2321,6 +2420,9 @@ TOML_NAMESPACE_START template inline constexpr bool is_node_view = impl::is_one_of, node_view, node_view>; + + template + inline constexpr bool is_string_like = impl::is_string_like; } TOML_NAMESPACE_END; @@ -4904,7 +5006,13 @@ 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-like' type (see toml::is_string_like)" \ + TOML_SA_LIST_BEG "with const char* data() const or const char* c_str() const" \ + TOML_SA_LIST_SEP "and size_t length() const" \ + TOML_SA_LIST_SEP "and constructible from std::string_view, (const char*, size_t) or const std::string&" \ + TOML_SA_LIST_END #define TOML_SA_VALUE_FUNC_MESSAGE(type_arg) \ "The " type_arg " must be one of:" \ @@ -4928,6 +5036,12 @@ 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-like' type (see toml::is_string_like)" \ + TOML_SA_LIST_BEG "with const char* data() const or const char* c_str() const" \ + TOML_SA_LIST_SEP "and size_t length() const" \ + TOML_SA_LIST_SEP "and constructible from std::string_view, (const char*, size_t) or const std::string&" \ TOML_SA_LIST_END // clang-format on @@ -4940,7 +5054,11 @@ TOML_IMPL_NAMESPACE_START TOML_NODISCARD static T make(Args&&... args) noexcept(std::is_nothrow_constructible_v) { - if constexpr (std::is_aggregate_v) + // user-defined string-likes (see toml::is_string_like) provide their character data + // via data()/c_str() + length() rather than by conversion + if constexpr (std::is_same_v && sizeof...(Args) == 1u && (is_string_like && ...)) + return std::string{ string_like_to_view(args)... }; + else if constexpr (std::is_aggregate_v) return T{ static_cast(args)... }; else return T(static_cast(args)...); @@ -5694,6 +5812,19 @@ TOML_NAMESPACE_START #endif } + // char -> user-defined string-like type (see toml::is_string_like) + else if constexpr (string_like_is_target) + { + // construction preference order mirrors string_like_target_is_nothrow + // (parenthesized init, matching the std::is_constructible checks) + if constexpr (std::is_constructible_v) + return T(std::string_view{ str }); + else if constexpr (std::is_constructible_v) + return T(str.data(), str.length()); + else + return T(str); + } + #if TOML_HAS_CHAR8 // char -> char8_t (potentially unsafe - the feature is 'experimental'!) @@ -5920,6 +6051,12 @@ TOML_NAMESPACE_START TOML_SA_LIST_SEP "const wchar_t*" #endif TOML_SA_LIST_END + + TOML_SA_LIST_NXT "A user-defined 'string-like' type (see toml::is_string_like)" + TOML_SA_LIST_BEG "with const char* data() const or const char* c_str() const" + TOML_SA_LIST_SEP "and size_t length() const" + TOML_SA_LIST_SEP "and constructible from std::string_view, (const char*, size_t) or const std::string&" + 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(): +