diff --git a/include/wil/stl.h b/include/wil/stl.h index d70c6889..976ba02f 100644 --- a/include/wil/stl.h +++ b/include/wil/stl.h @@ -160,6 +160,54 @@ inline wil::unique_bstr make_bstr(std::wstring_view source) #endif // WIL_ENABLE_EXCEPTIONS #endif // defined(__WIL_OLEAUTO_H_) +template > +class basic_zstring_view; + +/** + Traits policy for a basic_zstring_view whose constructors reject null pointers and whose default constructor points + at an internal empty string. The nested char_traits alias keeps the resulting basic_zstring_view derived from the + same std::basic_string_view specialization as the nullable form. + +*/ +template > +struct nonnull_zstring_view_traits +{ + using char_traits = Traits; + static constexpr bool empty_strings_are_non_null = true; +}; + +/// @cond +namespace details +{ + template + struct zstring_view_traits + { + static constexpr bool empty_strings_are_non_null = false; + using char_traits = Traits; + }; + + template + struct zstring_view_traits> + { + static constexpr bool empty_strings_are_non_null = Traits::empty_strings_are_non_null; + using char_traits = typename Traits::char_traits; + }; + + template + struct is_basic_zstring_view : std::false_type + { + }; + + template + struct is_basic_zstring_view> : std::true_type + { + }; + + template + inline constexpr TChar zstring_view_empty_storage[1]{TChar()}; +} // namespace details +/// @endcond + /** zstring_view. A zstring_view is identical to a std::string_view except it is always nul-terminated (unless empty). * zstring_view can be used for storing string literals without "forgetting" the length or that it is nul-terminated. @@ -169,11 +217,22 @@ inline wil::unique_bstr make_bstr(std::wstring_view source) * substr(pos) returns a zstring_view because the tail remains nul-terminated. substr(pos, count) returns a std::string_view because an arbitrary slice may not be nul-terminated. * contains() is available before C++23 through a compatibility implementation. + * nonnull_zstring_view uses a traits policy so its constructors produce non-null data(), including after default + construction. + + @note basic_zstring_view publicly inherits from std::basic_string_view. A caller can explicitly cast any + basic_zstring_view variant to a mutable base reference and assign data that is null or not nul-terminated. Avoid + mutating the object through a base reference. */ -template -class basic_zstring_view : public std::basic_string_view +template +class basic_zstring_view : public std::basic_string_view::char_traits> { - using size_type = typename std::basic_string_view::size_type; + using ZStringViewTraits = details::zstring_view_traits; + using BaseType = std::basic_string_view; + using size_type = typename BaseType::size_type; + + template + friend class basic_zstring_view; template struct has_c_str @@ -196,47 +255,103 @@ class basic_zstring_view : public std::basic_string_view }; public: - constexpr basic_zstring_view() noexcept = default; + constexpr basic_zstring_view() noexcept : BaseType(default_view()) + { + } constexpr basic_zstring_view(const basic_zstring_view&) noexcept = default; constexpr basic_zstring_view& operator=(const basic_zstring_view&) noexcept = default; constexpr basic_zstring_view(const TChar* pStringData, size_type stringLength) noexcept : - std::basic_string_view(pStringData, stringLength) + BaseType(view_from_pointer(pStringData, stringLength)) { - if (pStringData[stringLength] != 0) - { - WI_STL_FAIL_FAST_IF(true); - } + validate_pointer_and_terminator(); } template constexpr basic_zstring_view(const TChar (&stringArray)[stringArrayLength]) noexcept : - std::basic_string_view(&stringArray[0], length_n(&stringArray[0], stringArrayLength)) + BaseType(&stringArray[0], length_n(&stringArray[0], stringArrayLength)) { } + basic_zstring_view(std::nullptr_t) = delete; + // Construct from nul-terminated char ptr. To prevent this from overshadowing array construction, // we disable this constructor if the value is an array (including string literal). template ::value && !std::is_array::value>* = nullptr> - constexpr basic_zstring_view(TPtr&& pStr) noexcept : std::basic_string_view(std::forward(pStr)) + constexpr basic_zstring_view(TPtr&& pStr) noexcept : BaseType(view_from_pointer(std::forward(pStr))) + { + validate_pointer(); + } + + constexpr basic_zstring_view(const std::basic_string& str) noexcept : BaseType(&str[0], str.size()) { } - constexpr basic_zstring_view(const std::basic_string& str) noexcept : - std::basic_string_view(&str[0], str.size()) + template < + typename TSrc, + std::enable_if_t< + has_c_str::value && has_size::value && std::is_same_v && + !details::is_basic_zstring_view>::value>* = nullptr> + constexpr basic_zstring_view(TSrc const& src) noexcept : BaseType(view_from_pointer(src.c_str(), src.size())) { + validate_pointer(); } - template ::value && has_size::value && std::is_same_v>* = nullptr> - constexpr basic_zstring_view(TSrc const& src) noexcept : std::basic_string_view(src.c_str(), src.size()) + template < + typename TSrc, + std::enable_if_t< + has_c_str::value && !has_size::value && std::is_same_v && + !details::is_basic_zstring_view>::value>* = nullptr> + constexpr basic_zstring_view(TSrc const& src) noexcept : BaseType(view_from_pointer(src.c_str())) { + validate_pointer(); } - template ::value && !has_size::value && std::is_same_v>* = nullptr> - constexpr basic_zstring_view(TSrc const& src) noexcept : std::basic_string_view(src.c_str()) + template < + typename OtherTraits, + std::enable_if_t< + !std::is_same_v && std::is_same_v::BaseType> && + (!ZStringViewTraits::empty_strings_are_non_null || details::zstring_view_traits::empty_strings_are_non_null)>* = nullptr> + constexpr basic_zstring_view(const basic_zstring_view& other) noexcept : + BaseType(other.data(), other.size()) { } + template < + typename OtherTraits, + std::enable_if_t< + !std::is_same_v && std::is_same_v::BaseType> && + ZStringViewTraits::empty_strings_are_non_null && !details::zstring_view_traits::empty_strings_are_non_null>* = nullptr> + explicit constexpr basic_zstring_view(const basic_zstring_view& other) noexcept : + BaseType(view_from_pointer(other.data(), other.size())) + { + validate_pointer(); + } + + template ::BaseType>>* = nullptr> + basic_zstring_view(const basic_zstring_view&) = delete; + + template < + typename OtherTraits, + std::enable_if_t && std::is_same_v::BaseType>>* = nullptr> + constexpr basic_zstring_view& operator=(const basic_zstring_view& other) noexcept + { + const auto data = other.data(); + if constexpr (ZStringViewTraits::empty_strings_are_non_null && !details::zstring_view_traits::empty_strings_are_non_null) + { + if (data == nullptr) + { + WI_STL_FAIL_FAST_IF(data == nullptr); + return *this; + } + } + BaseType::operator=(BaseType(data, other.size())); + return *this; + } + + template ::BaseType>>* = nullptr> + basic_zstring_view& operator=(const basic_zstring_view&) = delete; + // basic_string_view [] precondition won't let us read view[view.size()]; so we define our own. WI_NODISCARD constexpr const TChar& operator[](size_type idx) const noexcept { @@ -246,6 +361,7 @@ class basic_zstring_view : public std::basic_string_view WI_NODISCARD constexpr const TChar* c_str() const noexcept { + WI_ASSERT(!ZStringViewTraits::empty_strings_are_non_null || (this->data() != nullptr)); WI_ASSERT(this->data() == nullptr || this->data()[this->size()] == 0); return this->data(); } @@ -253,7 +369,7 @@ class basic_zstring_view : public std::basic_string_view // contains() backport for builds below C++23. Compiles out once the STL provides // basic_string_view::contains natively. #if !defined(__cpp_lib_string_contains) || __cpp_lib_string_contains < 202011L - WI_NODISCARD constexpr bool contains(std::basic_string_view view) const noexcept + WI_NODISCARD constexpr bool contains(BaseType view) const noexcept { return this->find(view) != this->npos; } @@ -271,20 +387,83 @@ class basic_zstring_view : public std::basic_string_view WI_NODISCARD constexpr basic_zstring_view substr(size_type pos = 0) const { - const auto tail = std::basic_string_view(*this).substr(pos); + const auto tail = BaseType(*this).substr(pos); return tail.data() == nullptr ? basic_zstring_view{} : basic_zstring_view{tail.data(), tail.size()}; } - WI_NODISCARD constexpr std::basic_string_view substr(size_type pos, size_type count) const + WI_NODISCARD constexpr BaseType substr(size_type pos, size_type count) const { - return std::basic_string_view(*this).substr(pos, count); + return BaseType(*this).substr(pos, count); } private: + static constexpr BaseType default_view() noexcept + { + if constexpr (ZStringViewTraits::empty_strings_are_non_null) + { + return BaseType(&details::zstring_view_empty_storage[0], 0); + } + else + { + return BaseType{}; + } + } + + static constexpr BaseType view_from_pointer(const TChar* value) noexcept + { + if constexpr (ZStringViewTraits::empty_strings_are_non_null) + { + return BaseType(value, value == nullptr ? 0 : BaseType::traits_type::length(value)); + } + else + { + return BaseType(value); + } + } + + static constexpr BaseType view_from_pointer(const TChar* value, size_type length) noexcept + { + if constexpr (ZStringViewTraits::empty_strings_are_non_null) + { + // Let constructor-body validation report null without first passing an invalid range to the base. + return BaseType(value, value == nullptr ? 0 : length); + } + else + { + return BaseType(value, length); + } + } + + constexpr void validate_pointer() const noexcept + { + if constexpr (ZStringViewTraits::empty_strings_are_non_null) + { + WI_STL_FAIL_FAST_IF(this->data() == nullptr); + } + } + + constexpr void validate_pointer_and_terminator() const noexcept + { + const auto ptr = this->data(); + const auto len = this->size(); + if constexpr (ZStringViewTraits::empty_strings_are_non_null) + { + WI_STL_FAIL_FAST_IF((ptr == nullptr) || (ptr[len] != 0)); + } + else + { + // Preserve nullable preconditions; the guard also keeps valid construction constexpr. + if (ptr[len] != 0) + { + WI_STL_FAIL_FAST_IF(ptr[len] != 0); + } + } + } + // Bounds-checked version of char_traits::length, like strnlen. Requires that the input contains a null terminator. static constexpr size_type length_n(_In_reads_opt_(buf_size) const TChar* str, size_type buf_size) noexcept { - const std::basic_string_view view(str, buf_size); + const BaseType view(str, buf_size); auto pos = view.find_first_of(TChar()); if (pos == view.npos) { @@ -294,17 +473,21 @@ class basic_zstring_view : public std::basic_string_view } // The following basic_string_view methods must not be allowed because they break the nul-termination. - using std::basic_string_view::swap; - using std::basic_string_view::remove_suffix; + using BaseType::remove_suffix; + using BaseType::swap; }; using zstring_view = basic_zstring_view; using zwstring_view = basic_zstring_view; +// Variants that reject null construction and default to a non-null empty string. +using nonnull_zstring_view = basic_zstring_view>; +using nonnull_zwstring_view = basic_zstring_view>; + // str_raw_ptr is an overloaded function that retrieves a const pointer to the first character in a string's buffer. // This is the overload for std::wstring. Other overloads available in resource.h. -template -inline auto str_raw_ptr(basic_zstring_view str) +template +inline auto str_raw_ptr(basic_zstring_view str) { return str.c_str(); } @@ -423,8 +606,9 @@ overloaded(T...) -> overloaded; #ifndef WIL_SUPPRESS_STD_FORMAT_USE #if (__WI_LIBCPP_STD_VER >= 20) && WI_HAS_INCLUDE(, 1) // Assume present if C++20 #include -template -struct std::formatter, TChar> : std::formatter, TChar> +template +struct std::formatter, TChar> + : std::formatter::char_traits>, TChar> { }; #endif diff --git a/tests/StlTests.cpp b/tests/StlTests.cpp index 74c20246..262bb885 100644 --- a/tests/StlTests.cpp +++ b/tests/StlTests.cpp @@ -201,6 +201,20 @@ TEST_CASE("StlTests::TestZStringView formatting", "[stl][zstring_view]") auto fmtStr = std::format("Hello {}", str); REQUIRE(fmtStr == "Hello kittens"); } + + SECTION("nonnull_zstring_view can be used with std::format") + { + wil::nonnull_zstring_view str{"kittens"}; + auto fmtStr = std::format("Hello {}", str); + REQUIRE(fmtStr == "Hello kittens"); + } + + SECTION("nonnull_zwstring_view can be used with std::format") + { + wil::nonnull_zwstring_view str{L"kittens"}; + auto fmtStr = std::format(L"Hello {}", str); + REQUIRE(fmtStr == L"Hello kittens"); + } } #endif @@ -326,4 +340,225 @@ TEST_CASE("StlTests::TestZStringView substr and contains", "[stl][zstring_view]" test(wil::zwstring_view{L"Hello, World!"}, wil::zwstring_view{L"World!"}, L"Hello", L"missing", L'W', L'x'); } +TEST_CASE("StlTests::TestNonNullZStringView", "[stl][zstring_view][nonnull]") +{ + const auto test = [](auto nonnullDefault, auto nullableDefault, auto text) { + using nonnull_type = decltype(nonnullDefault); + using nullable_type = decltype(nullableDefault); + using char_type = typename nonnull_type::value_type; + using string_view_type = std::basic_string_view; + + STATIC_REQUIRE(sizeof(nonnull_type) == sizeof(nullable_type)); + STATIC_REQUIRE(std::is_trivially_copyable_v); + STATIC_REQUIRE(!std::is_constructible_v); + STATIC_REQUIRE(!std::is_constructible_v); + STATIC_REQUIRE(std::is_convertible_v); + STATIC_REQUIRE(!std::is_convertible_v); + STATIC_REQUIRE(std::is_constructible_v); + STATIC_REQUIRE(std::is_assignable_v); + STATIC_REQUIRE(std::is_assignable_v); + + REQUIRE(nullableDefault.data() == nullptr); + REQUIRE(nonnullDefault.data() != nullptr); + REQUIRE(nonnullDefault.empty()); + REQUIRE(nonnullDefault.c_str()[0] == char_type{}); + + nonnull_type fromLiteral{text}; + REQUIRE(fromLiteral.data() != nullptr); + REQUIRE(fromLiteral.c_str()[fromLiteral.size()] == char_type{}); + REQUIRE(wil::str_raw_ptr(fromLiteral) == fromLiteral.c_str()); + + string_view_type& baseReference = fromLiteral; + REQUIRE(baseReference.data() == fromLiteral.data()); + REQUIRE(baseReference.size() == fromLiteral.size()); + + nullable_type nullable = fromLiteral; + REQUIRE(nullable.data() == fromLiteral.data()); + REQUIRE(nullable.size() == fromLiteral.size()); + + nonnull_type checked{nullable}; + REQUIRE(checked.data() == nullable.data()); + REQUIRE(checked.size() == nullable.size()); + + nullable_type assigned; + assigned = fromLiteral; + REQUIRE(assigned.data() == fromLiteral.data()); + REQUIRE(assigned.size() == fromLiteral.size()); + + nonnull_type checkedAssignment; + REQUIRE(&(checkedAssignment = nullable) == &checkedAssignment); + REQUIRE(checkedAssignment.data() == nullable.data()); + REQUIRE(checkedAssignment.size() == nullable.size()); + + nonnull_type unchangedAfterRejectedAssignment{text}; + const auto originalData = unchangedAfterRejectedAssignment.data(); + const auto originalSize = unchangedAfterRejectedAssignment.size(); + REQUIRE_ERROR(unchangedAfterRejectedAssignment = nullableDefault); + REQUIRE(unchangedAfterRejectedAssignment.data() == originalData); + REQUIRE(unchangedAfterRejectedAssignment.size() == originalSize); + + auto emptyTail = nonnullDefault.substr(); + REQUIRE(emptyTail.data() != nullptr); + REQUIRE(emptyTail.empty()); + + const char_type* nullPointer = nullptr; + REQUIRE_ERROR((nonnull_type{nullPointer})); + REQUIRE_ERROR((nonnull_type{nullPointer, 0})); + REQUIRE_ERROR((nonnull_type{nullPointer, 1})); + REQUIRE_ERROR((nonnull_type{nullableDefault})); + }; + + test(wil::nonnull_zstring_view{}, wil::zstring_view{}, "hello"); + test(wil::nonnull_zwstring_view{}, wil::zwstring_view{}, L"hello"); + + struct custom_char_traits : std::char_traits + { + }; + using custom_nonnull = wil::basic_zstring_view>; + using custom_nullable = wil::basic_zstring_view; + using custom_base = std::basic_string_view::char_traits>; + STATIC_REQUIRE(std::is_base_of_v, custom_nonnull>); + STATIC_REQUIRE(!std::is_same_v); + STATIC_REQUIRE(!std::is_constructible_v); + STATIC_REQUIRE(!std::is_constructible_v); + STATIC_REQUIRE(!std::is_constructible_v); + STATIC_REQUIRE(!std::is_assignable_v); + STATIC_REQUIRE(!std::is_assignable_v); + STATIC_REQUIRE(!std::is_assignable_v); +} + +TEST_CASE("StlTests::ZStringView string-like null inputs", "[stl][zstring_view]") +{ + const auto test = [](auto nullableDefault, auto nonnullDefault, auto text) { + using nullable_type = decltype(nullableDefault); + using nonnull_type = decltype(nonnullDefault); + using char_type = typename nullable_type::value_type; + + struct sized_string + { + using value_type = char_type; + const char_type* data; + size_t length; + + constexpr const char_type* c_str() const noexcept + { + return data; + } + + constexpr size_t size() const noexcept + { + return length; + } + }; + + const sized_string emptyString{nullptr, 0}; + const nullable_type nullable{emptyString}; + REQUIRE(nullable.data() == nullptr); + REQUIRE(nullable.size() == 0); + REQUIRE(nullable.c_str() == nullptr); + REQUIRE_ERROR((nonnull_type{emptyString})); + REQUIRE_ERROR((nonnull_type{sized_string{nullptr, 1}})); + + const nullable_type copy{nullable}; + REQUIRE(copy.data() == nullptr); + REQUIRE(copy.size() == 0); + + nullable_type assigned{text}; + assigned = nullable; + REQUIRE(assigned.data() == nullptr); + REQUIRE(assigned.size() == 0); + REQUIRE_ERROR((nonnull_type{nullable})); + + nonnull_type destination{text}; + const auto originalData = destination.data(); + const auto originalSize = destination.size(); + REQUIRE_ERROR(destination = nullable); + REQUIRE(destination.data() == originalData); + REQUIRE(destination.size() == originalSize); + + const char_type emptyBuffer[]{char_type()}; + const sized_string bufferedEmptyString{emptyBuffer, 0}; + const nullable_type bufferedNullable{bufferedEmptyString}; + const nonnull_type bufferedNonnull{bufferedEmptyString}; + REQUIRE(bufferedNullable.data() == emptyBuffer); + REQUIRE(bufferedNullable.empty()); + REQUIRE(bufferedNonnull.data() == emptyBuffer); + REQUIRE(bufferedNonnull.empty()); + + struct path_like + { + using value_type = char_type; + constexpr const char_type* c_str() const noexcept + { + return nullptr; + } + }; + REQUIRE_ERROR((nonnull_type{path_like{}})); + }; + + test(wil::zstring_view{}, wil::nonnull_zstring_view{}, "hello"); + test(wil::zwstring_view{}, wil::nonnull_zwstring_view{}, L"hello"); +} + +TEST_CASE("StlTests::ZStringView zero-length buffer validation", "[stl][zstring_view]") +{ + const auto test = [](auto defaultView) { + using view_type = decltype(defaultView); + using char_type = typename view_type::value_type; + + const char_type emptyBuffer[]{char_type()}; + const view_type empty{emptyBuffer, 0}; + REQUIRE(empty.data() == emptyBuffer); + REQUIRE(empty.size() == 0); + REQUIRE(empty.c_str()[0] == char_type()); + + const char_type nonemptyBuffer[]{static_cast('x'), char_type()}; + REQUIRE_ERROR((view_type{nonemptyBuffer, 0})); + }; + + test(wil::zstring_view{}); + test(wil::zwstring_view{}); + test(wil::nonnull_zstring_view{}); + test(wil::nonnull_zwstring_view{}); +} + +TEST_CASE("StlTests::ZStringView constexpr pointer-length construction", "[stl][zstring_view]") +{ + const auto test = [](auto defaultView) { + using view_type = decltype(defaultView); + using char_type = typename view_type::value_type; + static constexpr char_type buffer[]{static_cast('a'), static_cast('b'), char_type()}; + + constexpr view_type view{buffer, 2}; + STATIC_REQUIRE(view.data() == buffer); + STATIC_REQUIRE(view.size() == 2); + + constexpr auto tail = view.substr(1); + STATIC_REQUIRE(tail.data() == buffer + 1); + STATIC_REQUIRE(tail.size() == 1); + + constexpr view_type empty{buffer + 2, 0}; + STATIC_REQUIRE(empty.data() == buffer + 2); + STATIC_REQUIRE(empty.empty()); + }; + + test(wil::zstring_view{}); + test(wil::zwstring_view{}); +} + +TEST_CASE("StlTests::TestZStringView partial policy detection", "[stl][zstring_view]") +{ + struct partial_policy + { + enum + { + empty_strings_are_non_null = true + }; + }; + + using traits = wil::details::zstring_view_traits; + STATIC_REQUIRE(!traits::empty_strings_are_non_null); + STATIC_REQUIRE(std::is_same_v); +} + #endif