From ba62fcca23137bb87a6a8602037aa2359608c105 Mon Sep 17 00:00:00 2001 From: Monroe Thomas Date: Wed, 19 Aug 2026 13:45:16 -0400 Subject: [PATCH 1/6] feat: add nonnull zstring_view variants Add a traits policy that preserves the underlying char_traits type while enforcing non-null construction. Provide narrow and wide aliases, checked cross-variant conversion, and focused invariant, reference-conversion, custom-traits, formatting, and fail-fast tests. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 5697cd0e-cf83-4d94-9f72-4b8c79379229 --- include/wil/stl.h | 180 ++++++++++++++++++++++++++++++++++++++------- tests/StlTests.cpp | 71 ++++++++++++++++++ 2 files changed, 226 insertions(+), 25 deletions(-) diff --git a/include/wil/stl.h b/include/wil/stl.h index d70c6889..388e514e 100644 --- a/include/wil/stl.h +++ b/include/wil/stl.h @@ -160,6 +160,58 @@ 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. + + @note basic_zstring_view publicly inherits from std::basic_string_view. A caller can explicitly cast to a mutable + base reference and assign a view with null data, bypassing the policy. Avoid mutating the object through a base + reference. +*/ +template > +struct nonnull_zstring_view_traits +{ + using char_traits = Traits; + static constexpr bool empty_strings_are_non_null = true; +}; + +namespace details +{ + template + struct zstring_view_traits + { + template + static std::bool_constant deduce_empty_strings_are_non_null(int); + template + static std::false_type deduce_empty_strings_are_non_null(...); + + template + static typename T::char_traits deduce_char_traits(int); + template + static T deduce_char_traits(...); + + static constexpr bool empty_strings_are_non_null = decltype(deduce_empty_strings_are_non_null(0))::value; + using char_traits = decltype(deduce_char_traits(0)); + }; + + 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 + /** 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 +221,15 @@ 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. */ -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 struct has_c_str @@ -196,14 +252,24 @@ 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(require_non_null(pStringData), stringLength) { - if (pStringData[stringLength] != 0) + if constexpr (ZStringViewTraits::empty_strings_are_non_null) + { + // The test harness records fail-fast and returns, so do not dereference a rejected null pointer afterward. + if ((pStringData != nullptr) && (pStringData[stringLength] != 0)) + { + WI_STL_FAIL_FAST_IF(true); + } + } + else if (pStringData[stringLength] != 0) { WI_STL_FAIL_FAST_IF(true); } @@ -211,29 +277,63 @@ class basic_zstring_view : public std::basic_string_view 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)) { } + template ::empty_strings_are_non_null, int> = 0> + 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(require_non_null(std::forward(pStr))) + { + } + + 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(require_non_null(src.c_str()), src.size()) { } - 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(require_non_null(src.c_str())) { } - 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::char_traits> && + (!ZStringViewTraits::empty_strings_are_non_null || details::zstring_view_traits::empty_strings_are_non_null), + int> = 0> + 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::char_traits> && + ZStringViewTraits::empty_strings_are_non_null && !details::zstring_view_traits::empty_strings_are_non_null, + long> = 0> + explicit constexpr basic_zstring_view(const basic_zstring_view& other) noexcept : + BaseType(require_non_null(other.data()), other.size()) { } @@ -253,7 +353,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 +371,45 @@ 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 const TChar* require_non_null(const TChar* value) noexcept + { + if constexpr (ZStringViewTraits::empty_strings_are_non_null) + { + if (value == nullptr) + { + WI_STL_FAIL_FAST_IF(true); + return &details::zstring_view_empty_storage[0]; + } + } + return value; + } + // 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 +419,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 +552,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..648863a1 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,61 @@ 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_convertible_v); + STATIC_REQUIRE(!std::is_convertible_v); + STATIC_REQUIRE(std::is_constructible_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()); + + 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{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>; + STATIC_REQUIRE(std::is_base_of_v, custom_nonnull>); +} + #endif From abfa08722607e1013fa49f81a7272ffded507139 Mon Sep 17 00:00:00 2001 From: Monroe Thomas Date: Wed, 19 Aug 2026 15:48:06 -0400 Subject: [PATCH 2/6] fix: fail fast on broken nonnull c_str contract Detect a null pointer when c_str() is called after mutation through the public string_view base, and cover the inheritance escape hatch with a regression test. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 5697cd0e-cf83-4d94-9f72-4b8c79379229 --- include/wil/stl.h | 4 ++++ tests/StlTests.cpp | 5 +++++ 2 files changed, 9 insertions(+) diff --git a/include/wil/stl.h b/include/wil/stl.h index 388e514e..2b7527a9 100644 --- a/include/wil/stl.h +++ b/include/wil/stl.h @@ -346,6 +346,10 @@ class basic_zstring_view : public std::basic_string_viewdata() == nullptr); + } WI_ASSERT(this->data() == nullptr || this->data()[this->size()] == 0); return this->data(); } diff --git a/tests/StlTests.cpp b/tests/StlTests.cpp index 648863a1..6933d5a6 100644 --- a/tests/StlTests.cpp +++ b/tests/StlTests.cpp @@ -369,6 +369,11 @@ TEST_CASE("StlTests::TestNonNullZStringView", "[stl][zstring_view][nonnull]") REQUIRE(baseReference.data() == fromLiteral.data()); REQUIRE(baseReference.size() == fromLiteral.size()); + nonnull_type corrupted{text}; + string_view_type& mutableBaseReference = corrupted; + mutableBaseReference = string_view_type{}; + REQUIRE_ERROR(corrupted.c_str()); + nullable_type nullable = fromLiteral; REQUIRE(nullable.data() == fromLiteral.data()); REQUIRE(nullable.size() == fromLiteral.size()); From 9043fbb8ecdd98ab51c7b58e785138b967fe9beb Mon Sep 17 00:00:00 2001 From: Monroe Thomas Date: Wed, 19 Aug 2026 15:57:48 -0400 Subject: [PATCH 3/6] fix: use pointer-specific fail-fast diagnostics Route nonnull pointer checks through FAIL_FAST_IF_NULL so diagnostics retain the checked expression and static analysis receives the pointer-specific contract. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 5697cd0e-cf83-4d94-9f72-4b8c79379229 --- include/wil/stl.h | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/include/wil/stl.h b/include/wil/stl.h index 2b7527a9..3a2b0b3c 100644 --- a/include/wil/stl.h +++ b/include/wil/stl.h @@ -27,6 +27,9 @@ #ifndef WI_STL_FAIL_FAST_IF #define WI_STL_FAIL_FAST_IF FAIL_FAST_IF #endif +#ifndef WI_STL_FAIL_FAST_IF_NULL +#define WI_STL_FAIL_FAST_IF_NULL FAIL_FAST_IF_NULL +#endif /// @endcond #if defined(WIL_ENABLE_EXCEPTIONS) @@ -348,7 +351,7 @@ class basic_zstring_view : public std::basic_string_viewdata() == nullptr); + WI_STL_FAIL_FAST_IF_NULL(this->data()); } WI_ASSERT(this->data() == nullptr || this->data()[this->size()] == 0); return this->data(); @@ -401,9 +404,9 @@ class basic_zstring_view : public std::basic_string_view[0]; } } From 4a151199eadde0d09ab3e721164fa407a04d7ac7 Mon Sep 17 00:00:00 2001 From: Monroe Thomas Date: Wed, 19 Aug 2026 16:23:23 -0400 Subject: [PATCH 4/6] refactor: align nonnull traits with review guidance Gate cross-policy conversions on the exact string_view base type, reject incompatible specializations, and limit rebinding to explicitly marked policy traits. Use a debug assertion rather than a partial production fail-fast for base-class mutation. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 5697cd0e-cf83-4d94-9f72-4b8c79379229 --- include/wil/stl.h | 37 ++++++++++++++++++++----------------- tests/StlTests.cpp | 11 ++++++----- 2 files changed, 26 insertions(+), 22 deletions(-) diff --git a/include/wil/stl.h b/include/wil/stl.h index 3a2b0b3c..2dbaac7d 100644 --- a/include/wil/stl.h +++ b/include/wil/stl.h @@ -184,21 +184,18 @@ struct nonnull_zstring_view_traits namespace details { - template + template struct zstring_view_traits { - template - static std::bool_constant deduce_empty_strings_are_non_null(int); - template - static std::false_type deduce_empty_strings_are_non_null(...); - - template - static typename T::char_traits deduce_char_traits(int); - template - static T deduce_char_traits(...); + static constexpr bool empty_strings_are_non_null = false; + using char_traits = Traits; + }; - static constexpr bool empty_strings_are_non_null = decltype(deduce_empty_strings_are_non_null(0))::value; - using char_traits = decltype(deduce_char_traits(0)); + 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 @@ -234,6 +231,9 @@ class basic_zstring_view : public std::basic_string_view; using size_type = typename BaseType::size_type; + template + friend class basic_zstring_view; + template struct has_c_str { @@ -319,8 +319,7 @@ class basic_zstring_view : public std::basic_string_view && - std::is_same_v::char_traits> && + !std::is_same_v && std::is_same_v::BaseType> && (!ZStringViewTraits::empty_strings_are_non_null || details::zstring_view_traits::empty_strings_are_non_null), int> = 0> constexpr basic_zstring_view(const basic_zstring_view& other) noexcept : @@ -331,8 +330,7 @@ class basic_zstring_view : public std::basic_string_view && - std::is_same_v::char_traits> && + !std::is_same_v && std::is_same_v::BaseType> && ZStringViewTraits::empty_strings_are_non_null && !details::zstring_view_traits::empty_strings_are_non_null, long> = 0> explicit constexpr basic_zstring_view(const basic_zstring_view& other) noexcept : @@ -340,6 +338,11 @@ class basic_zstring_view : public std::basic_string_view && !std::is_same_v::BaseType>, short> = 0> + basic_zstring_view(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 { @@ -351,7 +354,7 @@ class basic_zstring_view : public std::basic_string_viewdata()); + WI_ASSERT(this->data() != nullptr); } WI_ASSERT(this->data() == nullptr || this->data()[this->size()] == 0); return this->data(); diff --git a/tests/StlTests.cpp b/tests/StlTests.cpp index 6933d5a6..172b2fde 100644 --- a/tests/StlTests.cpp +++ b/tests/StlTests.cpp @@ -369,11 +369,6 @@ TEST_CASE("StlTests::TestNonNullZStringView", "[stl][zstring_view][nonnull]") REQUIRE(baseReference.data() == fromLiteral.data()); REQUIRE(baseReference.size() == fromLiteral.size()); - nonnull_type corrupted{text}; - string_view_type& mutableBaseReference = corrupted; - mutableBaseReference = string_view_type{}; - REQUIRE_ERROR(corrupted.c_str()); - nullable_type nullable = fromLiteral; REQUIRE(nullable.data() == fromLiteral.data()); REQUIRE(nullable.size() == fromLiteral.size()); @@ -399,7 +394,13 @@ TEST_CASE("StlTests::TestNonNullZStringView", "[stl][zstring_view][nonnull]") { }; 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); } #endif From e60a42a40a51063e7e9c19316ba9f86b81d32de3 Mon Sep 17 00:00:00 2001 From: Monroe Thomas Date: Mon, 14 Sep 2026 13:28:35 -0400 Subject: [PATCH 5/6] refactor: simplify nonnull zstring validation and assignment Validate pointer inputs in constructor bodies without substituting test-only buffers. Reject direct nullptr construction for both variants, tighten policy detection, and clarify the shared inheritance limitation. Add compatible cross-policy assignment with pre-mutation null validation, preserving the destination when fail-fast is intercepted. Cover nullable nullptr rejection, partial policies, assignment mutation, and rejected-assignment preservation. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 5697cd0e-cf83-4d94-9f72-4b8c79379229 --- include/wil/stl.h | 101 ++++++++++++++++++++++++--------------------- tests/StlTests.cpp | 40 ++++++++++++++++++ 2 files changed, 95 insertions(+), 46 deletions(-) diff --git a/include/wil/stl.h b/include/wil/stl.h index 2dbaac7d..f3b9a90a 100644 --- a/include/wil/stl.h +++ b/include/wil/stl.h @@ -27,9 +27,6 @@ #ifndef WI_STL_FAIL_FAST_IF #define WI_STL_FAIL_FAST_IF FAIL_FAST_IF #endif -#ifndef WI_STL_FAIL_FAST_IF_NULL -#define WI_STL_FAIL_FAST_IF_NULL FAIL_FAST_IF_NULL -#endif /// @endcond #if defined(WIL_ENABLE_EXCEPTIONS) @@ -171,9 +168,6 @@ class basic_zstring_view; 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. - @note basic_zstring_view publicly inherits from std::basic_string_view. A caller can explicitly cast to a mutable - base reference and assign a view with null data, bypassing the policy. Avoid mutating the object through a base - reference. */ template > struct nonnull_zstring_view_traits @@ -182,6 +176,7 @@ struct nonnull_zstring_view_traits static constexpr bool empty_strings_are_non_null = true; }; +/// @cond namespace details { template @@ -192,7 +187,7 @@ namespace details }; template - struct zstring_view_traits> + 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; @@ -211,6 +206,7 @@ namespace details 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). @@ -223,6 +219,10 @@ namespace details * 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::char_traits> @@ -261,21 +261,9 @@ class basic_zstring_view : public std::basic_string_view @@ -284,14 +272,14 @@ class basic_zstring_view : public std::basic_string_view::empty_strings_are_non_null, int> = 0> 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 : BaseType(require_non_null(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()) @@ -303,8 +291,9 @@ class basic_zstring_view : public std::basic_string_view::value && has_size::value && std::is_same_v && !details::is_basic_zstring_view>::value>* = nullptr> - constexpr basic_zstring_view(TSrc const& src) noexcept : BaseType(require_non_null(src.c_str()), src.size()) + constexpr basic_zstring_view(TSrc const& src) noexcept : BaseType(src.c_str(), src.size()) { + validate_pointer(); } template < @@ -312,16 +301,16 @@ class basic_zstring_view : public std::basic_string_view::value && !has_size::value && std::is_same_v && !details::is_basic_zstring_view>::value>* = nullptr> - constexpr basic_zstring_view(TSrc const& src) noexcept : BaseType(require_non_null(src.c_str())) + constexpr basic_zstring_view(TSrc const& src) noexcept : BaseType(view_from_pointer(src.c_str())) { + validate_pointer(); } 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), - int> = 0> + (!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()) { @@ -331,17 +320,36 @@ class basic_zstring_view : public std::basic_string_view && std::is_same_v::BaseType> && - ZStringViewTraits::empty_strings_are_non_null && !details::zstring_view_traits::empty_strings_are_non_null, - long> = 0> + 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(require_non_null(other.data()), other.size()) + BaseType(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>, short> = 0> - basic_zstring_view(const basic_zstring_view&) = delete; + 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 @@ -352,10 +360,7 @@ class basic_zstring_view : public std::basic_string_viewdata() != nullptr); - } + WI_ASSERT(!ZStringViewTraits::empty_strings_are_non_null || (this->data() != nullptr)); WI_ASSERT(this->data() == nullptr || this->data()[this->size()] == 0); return this->data(); } @@ -403,17 +408,21 @@ class basic_zstring_view : public std::basic_string_view[0]; - } - } - return value; + return BaseType(value, value == nullptr ? 0 : BaseType::traits_type::length(value)); + } + + constexpr void validate_pointer() const noexcept + { + 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(); + WI_STL_FAIL_FAST_IF((ptr == nullptr) || (ptr[len] != 0)); } // Bounds-checked version of char_traits::length, like strnlen. Requires that the input contains a null terminator. diff --git a/tests/StlTests.cpp b/tests/StlTests.cpp index 172b2fde..68dd2d4c 100644 --- a/tests/StlTests.cpp +++ b/tests/StlTests.cpp @@ -351,9 +351,12 @@ TEST_CASE("StlTests::TestNonNullZStringView", "[stl][zstring_view][nonnull]") 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); @@ -377,11 +380,30 @@ TEST_CASE("StlTests::TestNonNullZStringView", "[stl][zstring_view][nonnull]") 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((nullable_type{nullPointer})); + REQUIRE_ERROR((nullable_type{nullPointer, 0})); REQUIRE_ERROR((nonnull_type{nullPointer})); REQUIRE_ERROR((nonnull_type{nullPointer, 0})); REQUIRE_ERROR((nonnull_type{nullableDefault})); @@ -401,6 +423,24 @@ TEST_CASE("StlTests::TestNonNullZStringView", "[stl][zstring_view][nonnull]") 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::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 From 92ef2221ec53dce1d48fa14737d0e855ff257e25 Mon Sep 17 00:00:00 2001 From: Monroe Thomas Date: Wed, 16 Sep 2026 10:58:08 -0400 Subject: [PATCH 6/6] fix: preserve nullable zstring view construction behavior Limit runtime null validation and guarded base initialization to the nonnull policy. Preserve nullable construction paths and allow empty string-like objects to retain null data. Keep the nullable terminator guard so valid construction and substr remain constexpr-usable while reporting the actual failing expression. Cover null and zero-length string-like inputs, nonnull null/nonzero rejection, copied and assigned nullable state, empty buffers, and compile-time narrow/wide construction. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 5697cd0e-cf83-4d94-9f72-4b8c79379229 --- include/wil/stl.h | 47 ++++++++++++++--- tests/StlTests.cpp | 122 ++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 161 insertions(+), 8 deletions(-) diff --git a/include/wil/stl.h b/include/wil/stl.h index f3b9a90a..976ba02f 100644 --- a/include/wil/stl.h +++ b/include/wil/stl.h @@ -261,7 +261,8 @@ class basic_zstring_view : public std::basic_string_view::value && has_size::value && std::is_same_v && !details::is_basic_zstring_view>::value>* = nullptr> - constexpr basic_zstring_view(TSrc const& src) noexcept : BaseType(src.c_str(), src.size()) + constexpr basic_zstring_view(TSrc const& src) noexcept : BaseType(view_from_pointer(src.c_str(), src.size())) { validate_pointer(); } @@ -322,7 +323,7 @@ class basic_zstring_view : public std::basic_string_view && 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(other.data(), other.size()) + BaseType(view_from_pointer(other.data(), other.size())) { validate_pointer(); } @@ -410,19 +411,53 @@ class basic_zstring_view : public std::basic_string_viewdata() == nullptr); + 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(); - WI_STL_FAIL_FAST_IF((ptr == nullptr) || (ptr[len] != 0)); + 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. diff --git a/tests/StlTests.cpp b/tests/StlTests.cpp index 68dd2d4c..262bb885 100644 --- a/tests/StlTests.cpp +++ b/tests/StlTests.cpp @@ -402,10 +402,9 @@ TEST_CASE("StlTests::TestNonNullZStringView", "[stl][zstring_view][nonnull]") REQUIRE(emptyTail.empty()); const char_type* nullPointer = nullptr; - REQUIRE_ERROR((nullable_type{nullPointer})); - REQUIRE_ERROR((nullable_type{nullPointer, 0})); REQUIRE_ERROR((nonnull_type{nullPointer})); REQUIRE_ERROR((nonnull_type{nullPointer, 0})); + REQUIRE_ERROR((nonnull_type{nullPointer, 1})); REQUIRE_ERROR((nonnull_type{nullableDefault})); }; @@ -428,6 +427,125 @@ TEST_CASE("StlTests::TestNonNullZStringView", "[stl][zstring_view][nonnull]") 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