diff --git a/include/wil/stl.h b/include/wil/stl.h index 5abd78d2..fee4734c 100644 --- a/include/wil/stl.h +++ b/include/wil/stl.h @@ -134,17 +134,150 @@ inline PCWSTR str_raw_ptr(const std::wstring& str) #if __cpp_lib_string_view >= 201606L +/// @cond +namespace details +{ + // SFINAE detector for the `empty_strings_are_non_null` opt-in marker on a Traits type. + // Mirrors MSVC STL's `_Get_propagate_on_container_copy` allocator_traits detection idiom. + template + struct zsv_empty_is_nonnull : std::false_type + { + }; + + template + struct zsv_empty_is_nonnull> : T::empty_strings_are_non_null + { + }; + + template + inline constexpr bool zsv_empty_is_nonnull_v = zsv_empty_is_nonnull::value; + + // Sentinel buffer for the safe variant's default-constructed view. Variable template so + // it's lazily instantiated and only emitted for TChars actually default-constructed + // through a safe `basic_zstring_view`; the default variant never references it. + // Parameterized on TChar (not Traits) so all safe instantiations sharing a char type + // COMDAT-fold onto a single byte of `.rdata`. + template + inline constexpr TChar zsv_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. - * A zstring_view can be converted implicitly to a std::string_view because it is always safe to use a nul-terminated - string_view as a plain string view. - * A zstring_view can be constructed from a std::string because the data in std::string is nul-terminated. + `zstring_view_traits_safe` is a `std::char_traits`-compatible traits type that opts + `basic_zstring_view` into the "non-null empty buffer" behaviour proposed for + `std::basic_zstring_view` in P3655R0. Instantiating + `basic_zstring_view>` (aliased as `wil::zstring_view_safe` + / `wil::zwstring_view_safe`) yields a view whose `data() != nullptr` is invariant: a + default-constructed view points at an internal static empty buffer and `c_str()[0] == TChar()` + is always safe to dereference. + + The nested type `empty_strings_are_non_null = std::true_type;` is detected by + `wil::details::zsv_empty_is_nonnull_v`. Default char_traits do not carry the marker, so + `basic_zstring_view` (with the default traits) retains the original WIL semantics where + a default-constructed view has `data() == nullptr`. */ template -class basic_zstring_view : public std::basic_string_view +struct zstring_view_traits_safe : std::char_traits { - using size_type = typename std::basic_string_view::size_type; + using empty_strings_are_non_null = std::true_type; +}; + +/** + `basic_zstring_view` is a non-owning, read-only view of a *null-terminated* + sequence of `TChar`. The class adds null-termination guarantees to `std::basic_string_view`, + making it suitable for passing to C APIs that require dereferenceable null-terminated strings + (e.g. `printf("%s", v.c_str())`, `fopen(v.c_str(), ...)`). + + The `Traits` template parameter selects between two behavioural variants. Default + (`Traits = std::char_traits`) preserves the original WIL semantics: a default-constructed + view has `data() == nullptr` and `c_str()` returns null. The opt-in safe variant + (`Traits = zstring_view_traits_safe`) makes `data() != nullptr` invariant: default + construction points at an internal static empty buffer and `c_str()` is always safe to + dereference. Type aliases `wil::zstring_view` / `wil::zwstring_view` select the default + variant; `wil::zstring_view_safe` / `wil::zwstring_view_safe` select the safe variant. Both + variants can be used in the same translation unit; the choice is per-call-site, not per-binary. + + Both variants: + - Construct from a string literal, a `(const TChar*, size_type)` pair (with a fail-fast + verifying the null terminator), a `std::basic_string`, or any user-defined type that + exposes `c_str()` (and optionally `size()`) returning `TChar`. The safe variant + additionally fail-fasts on a null `pStringData`; the default variant treats null as + a precondition violation (latent UB on the null-terminator deref), matching the prior + WIL class behavior. The safe variant also `= delete`s the `(std::nullptr_t)` overload + (tracks P3655R0 and P2166R1's direction); the default variant leaves the overload + accessible so existing C++17/C++20 call sites that construct from a `nullptr` literal + continue to compile (such code was always latent UB at first use, but breaking it now + would be a source-compat regression for existing WIL consumers). + - Provide an explicit converting constructor from `std::basic_string_view` that + fail-fasts on a null pointer (the only way `std::basic_string_view` can violate the + non-null contract) and then delegates to the validating `(ptr, len)` ctor. + - `substr(pos)` returns a `basic_zstring_view` (the tail of a null-terminated string is + itself null-terminated); follows the design proposed for `std::zstring_view` in P3655R0. + - `substr(pos, count)` returns a `std::basic_string_view` because an + arbitrary slice is generally not null-terminated. + + @note **Slicing risk.** Public inheritance from `std::basic_string_view` means + a caller can bypass the null-termination invariant by binding to the base, e.g. + `static_cast&>(zv) = sv` where `sv` is a non-null-terminated + view. This also re-exposes hidden invariant-breakers (`remove_suffix`, `swap`) on the slice. + P3655R0's proposed `std::zstring_view` avoids this by not inheriting; WIL retains inheritance + for source compatibility with prior versions of this class and for broad-ecosystem + `std::basic_string_view` interop. The invariant holds for normal use but is not airtight + against base-class slicing. + + @note **Hidden (not deleted) invariant-breakers.** `remove_suffix(n)` and `swap(other)` are + inaccessible from `basic_zstring_view` directly (compile error: "inaccessible") via private + using-declarations. They remain reachable via base-class slice/cast (see slicing caveat + above). The 2-arg `substr(pos, count)` is permitted but returns the base + `std::basic_string_view` because an arbitrary slice may not be null-terminated. + + @note **Cross-instantiation conversion.** + - `zstring_view_safe` converts **implicitly** to `zstring_view` - the more-constrained type + collapses into the less-constrained one without runtime cost. The collapse is sound + because `zstring_view_traits_safe` derives from `std::char_traits` with no + behavioural overrides, so the two `Traits` have identical `compare`/`eq`/`length` + semantics. The shape of the asymmetry (more-constrained implicit, less-constrained + explicit) parallels `std::span` -> `std::span` (C++20). + - The reverse direction (default -> safe) is an **explicit** converting constructor on the + safe variant: `zstring_view_safe{view}`. It delegates to the safe variant's validating + `(pointer, length)` constructor, which fail-fasts on a null pointer (so calling it on a + default-constructed `zstring_view` - whose `data()` is null - fail-fasts on conversion). + The `explicit` keyword keeps the safety transition visible at the call site. + - The safe variant also converts **implicitly** to `std::basic_string_view` (default + traits), enabling drop-in use with any API taking `std::string_view`. This is a + pointer+size rewrap; the safe variant's invariants are already enforced before conversion, + so the resulting `std::string_view` is always non-null (though the receiver does not know + that statically). The conversion is gated to the canonical + `zstring_view_traits_safe` (not any `Traits` that merely derives from it): a user + who extends the traits with custom `eq`/`lt`/`compare` (e.g. case-insensitive) is not + silently demoted to default `char_traits` semantics on conversion. + - Cross-variant comparison works out of the box: a `zstring_view_safe` can be compared to a + `zstring_view` (and vice versa). Both directions bind to `std::basic_string_view`'s + `operator==` via the existing implicit conversions (safe -> base via + `operator basic_string_view()`, default -> base via inherited-base slicing). No + special cross-Traits overloads needed. + + @note **Overload-resolution caveat (safe variant only).** Because + `zstring_view_safe -> zstring_view` and `zstring_view_safe -> std::basic_string_view` + are both single user-defined conversions, a call site that has both `f(wil::zstring_view)` and + `f(std::string_view)` overloads will be ambiguous when passed a `zstring_view_safe`. + Disambiguate at the call site with `f(wil::zstring_view{safe_view})` or + `f(std::string_view{safe_view})`. The ambiguity does not arise for the default variant + (identity match wins) or when only one of the two overloads is present. + + @note **Safe variant `(nullptr, 0)` policy.** The safe variant's `(ptr, len)` ctor fail-fasts + on a null pointer regardless of length; `(nullptr, 0)` is not a "harmless empty" shortcut. + P3655R0's invariant - `[data(), data() + size()]` is a valid range with `data() + size()` + pointing at `charT()` - cannot be satisfied when `data()` is null, even at size zero. Same + `WI_STL_FAIL_FAST_IF` primitive (and same `REQUIRE_ERROR`-style testability) as the existing + no-NULL-in-range fail-fast that both variants have shipped for years. Callers wanting "empty + intent" should default-construct (`zstring_view_safe{}`), which yields a dereferenceable empty + buffer at zero runtime cost. +*/ +template > +class basic_zstring_view : public std::basic_string_view +{ + using size_type = typename std::basic_string_view::size_type; template struct has_c_str @@ -166,13 +299,59 @@ class basic_zstring_view : public std::basic_string_view static constexpr bool value = decltype(test(0))::value; }; + // Tag for the trusted (non-validating) private (ptr, len) ctor used by substr() and other + // internal sites that have already established the invariants. + struct _trusted_tag + { + }; + public: - constexpr basic_zstring_view() noexcept = default; + /** + * Default-construct a view. + * - Default Traits (`zstring_view` / `zwstring_view`): `data() == nullptr`, `size() == 0` - + * matches `std::basic_string_view`. + * - Safe Traits (`zstring_view_safe` / `zwstring_view_safe`): `data() != nullptr`, + * `size() == 0`, `c_str()[0] == TChar()` - the view points at an internal static empty + * buffer and is safe to hand to any C API expecting a dereferenceable null-terminated string. + */ + constexpr basic_zstring_view() noexcept + { + if constexpr (details::zsv_empty_is_nonnull_v) + { + // Default-init of the base produced (nullptr, 0); re-seat with the sentinel. + static_cast&>(*this) = + std::basic_string_view(&details::zsv_empty_storage[0], 0); + } + } + 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) + // Deleted on the safe variant - tracks the standard direction (P2166R1 deletes the + // analogous overload on `basic_string_view` in C++23; P3655R0 carries the same rule + // through for the proposed `std::zstring_view`). Left intact on the default variant so + // existing C++17/C++20 callers that pass a `nullptr` literal continue to compile (the + // resulting view is latent UB at first use, but breaking that source pattern now would + // be a compat regression for existing WIL consumers). + template , int> = 0> + basic_zstring_view(std::nullptr_t) = delete; + + // (ptr, len) ctor. Both variants take `_In_reads_z_` (non-null is part of the contract); the + // variants differ only in runtime enforcement of that contract, and in how the two diverge + // under test-harness fail-fast detours (e.g. witest's `DoesCodeFailFast`, which intercepts + // `ReportFailure_NoReturn` and returns rather than aborting): + // - Default Traits: trusts the caller. Null is latent UB on the null-terminator deref below. + // Matches the prior WIL class behavior. The test suite deliberately never passes + // `(nullptr, N)` to this variant - there is no fail-fast to detour, so a test would just + // real-AV. + // - Safe Traits: fail-fasts on a null pointer via `_require_non_null` in the init list, so + // the diagnosis runs before the base ctor sees a malformed `(ptr, len)` pair. The body + // additionally guards the trailing-null read on `pStringData != nullptr` so a detoured + // fail-fast returns instead of real-AVing after `_require_non_null` hands back the (null) + // pointer. + template , int> = 0> + constexpr basic_zstring_view(_In_reads_z_(stringLength + 1) const TChar* pStringData, size_type stringLength) noexcept : + std::basic_string_view(pStringData, stringLength) { if (pStringData[stringLength] != 0) { @@ -180,34 +359,113 @@ class basic_zstring_view : public std::basic_string_view } } + template , int> = 0> + constexpr basic_zstring_view(_In_reads_z_(stringLength + 1) const TChar* pStringData, size_type stringLength) noexcept : + std::basic_string_view(_require_non_null(pStringData), stringLength) + { + if (pStringData != nullptr && pStringData[stringLength] != 0) + { + WI_STL_FAIL_FAST_IF(true); + } + } + template constexpr basic_zstring_view(const TChar (&stringArray)[stringArrayLength]) noexcept : - std::basic_string_view(&stringArray[0], length_n(&stringArray[0], stringArrayLength)) + std::basic_string_view(&stringArray[0], length_n(&stringArray[0], stringArrayLength)) { } // 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). + // Note: no `_In_z_` annotation because TPtr can be a class type with a conversion operator, + // so the SAL annotation does not apply cleanly. 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 : std::basic_string_view(std::forward(pStr)) { } constexpr basic_zstring_view(const std::basic_string& str) noexcept : - std::basic_string_view(&str[0], str.size()) + std::basic_string_view(&str[0], str.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()) + constexpr basic_zstring_view(TSrc const& src) noexcept : std::basic_string_view(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()) + constexpr basic_zstring_view(TSrc const& src) noexcept : std::basic_string_view(src.c_str()) + { + } + + /** + * Explicit converting constructor from `std::basic_string_view`. Fail-fasts if the + * source view has `data() == nullptr` (the only way `std::basic_string_view` can violate the + * non-null contract); otherwise delegates to the validating `(ptr, len)` ctor. + */ + constexpr explicit basic_zstring_view(std::basic_string_view sv) noexcept : + basic_zstring_view(_require_non_null(sv.data()), sv.size()) + { + } + + // Cross-variant converting ctors. SFINAE-gated so each direction exists only on the relevant + // variant: + // - On the default variant: implicit ctor accepting the safe variant. The collapse is sound + // because `zstring_view_traits_safe` derives from `std::char_traits` with no + // behavioural overrides, so the two Traits have identical compare/eq/length semantics. The + // shape of the asymmetry (more-constrained implicit, less-constrained explicit) parallels + // `std::span` -> `std::span` (C++20). + // - On the safe variant: explicit ctor accepting the default variant (delegates to the + // validating (ptr, len) ctor, which fail-fasts on null). + // NOTE: these cross-variant ctors take their argument by const-reference rather than by value. + // The standard treats a ctor whose first parameter is the enclosing class type as a copy ctor + // and forbids the by-value form (it would recurse infinitely). For the default-Traits + // instantiation `basic_zstring_view>` the line below would be + // exactly that by-value copy ctor signature if we took the argument by value - the diagnostic + // ("illegal copy constructor: first parameter must not be ...") fires before SFINAE has a + // chance to remove the overload, so by-const-ref is the only signature that compiles. + template , int> = 0> + constexpr basic_zstring_view(basic_zstring_view> const& safe) noexcept : + std::basic_string_view(safe.data(), safe.size()) { } + template , int> = 0> + constexpr explicit basic_zstring_view(basic_zstring_view> const& other) noexcept : + basic_zstring_view(other.data(), other.size()) + { + } + + // Implicit conversion to default-traits `std::basic_string_view`. + // + // Only present on the safe variant. For the shipped default aliases (`wil::zstring_view` / + // `wil::zwstring_view`, where `Traits = std::char_traits`), the inherited base IS + // already `std::basic_string_view` so slicing provides this conversion for free; + // adding the operator there would be redundant. (A user-instantiated default variant with + // custom Traits has a different inherited base and would slice to that base instead.) + // + // Only present when `Traits` is exactly `zstring_view_traits_safe` (not any Traits + // that merely carries the safe marker). A user who extends `zstring_view_traits_safe` + // with custom eq/lt/compare (e.g. case-insensitive) would otherwise silently lose those + // semantics when converted to default-traits `std::basic_string_view`. The + // std library deliberately omits implicit `basic_string_view` -> + // `basic_string_view` conversions for the same reason. Extenders who genuinely + // want the lossy conversion can write `std::basic_string_view(view.data(), view.size())` + // (one line, intent visible). + // + // The return type is hidden behind `enable_if_t` to make it template-dependent on `T`. This + // sidesteps clang's `-Wclass-conversion` warning, which fires syntactically on the default-Traits + // instantiation (where `std::basic_string_view` happens to be the inherited base) before + // SFINAE removes the operator from the candidate set. A template-dependent return type forces + // clang to defer the base-class shape check until template-argument deduction, at which point + // SFINAE has already eliminated the candidate for default-Traits. + template + constexpr operator std::enable_if_t && std::is_same_v>, std::basic_string_view>() const noexcept + { + return {this->data(), this->size()}; + } + // 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 { @@ -215,17 +473,103 @@ class basic_zstring_view : public std::basic_string_view return this->data()[idx]; } - WI_NODISCARD constexpr const TChar* c_str() const noexcept + // `_Ret_maybenull_z_` rather than per-Traits split: SAL is a preprocessor-time annotation + // on the declaration and can't vary with template parameters resolved at instantiation, + // and per-Traits SFINAE overloads would change c_str()'s mangled name (source-compat hazard + // for any caller taking `&basic_zstring_view::c_str`). `_Ret_maybenull_z_` is truthful for + // both variants (loose but correct for the safe variant, whose invariant guarantees non-null). + // Callers of the safe variant who want analyzer help can assert non-null at the call site. + WI_NODISCARD _Ret_maybenull_z_ constexpr const TChar* c_str() const noexcept { WI_ASSERT(this->data() == nullptr || this->data()[this->size()] == 0); return this->data(); } + // contains() backport for builds below C++23. Compiles out once the STL provides + // basic_string_view::contains natively (via __cpp_lib_string_contains). +#if !defined(__cpp_lib_string_contains) || __cpp_lib_string_contains < 202011L + WI_NODISCARD constexpr bool contains(std::basic_string_view sv) const noexcept + { + return std::basic_string_view(*this).find(sv) != std::basic_string_view::npos; + } + + WI_NODISCARD constexpr bool contains(TChar ch) const noexcept + { + return std::basic_string_view(*this).find(ch) != std::basic_string_view::npos; + } + + WI_NODISCARD constexpr bool contains(_In_z_ const TChar* s) const + { + return std::basic_string_view(*this).find(s) != std::basic_string_view::npos; + } +#endif // !defined(__cpp_lib_string_contains) || __cpp_lib_string_contains < 202011L + + /** + * Returns a `basic_zstring_view` of the tail of this view, starting at `pos`. + * + * The result is null-terminated because the tail of a null-terminated string is itself + * null-terminated. + * + * @param pos starting position (default 0). Must satisfy `pos <= size()`. + * @throws std::out_of_range if `pos > size()` (propagated from `std::basic_string_view::substr`). + */ + WI_NODISCARD constexpr basic_zstring_view substr(size_type pos = 0) const + { + const auto tail = std::basic_string_view(*this).substr(pos); + if constexpr (!details::zsv_empty_is_nonnull_v) + { + // Short-circuit the (nullptr, 0) tail case (substr(0) on a default-constructed + // default-traits view) so we don't round-trip a null pointer through the trusted + // ctor's base initializer. + if (tail.data() == nullptr) + { + return basic_zstring_view{}; + } + } + // Already validated by construction; use the trusted ctor to skip re-validation. + return basic_zstring_view(_trusted_tag{}, tail.data(), tail.size()); + } + + // Re-declared (not just inherited) so the one-arg substr(pos) above doesn't hide the + // inherited two-arg overload via standard name-hiding rules. + /** + * Returns a `std::basic_string_view` of an arbitrary sub-range. + * + * The return type is `std::basic_string_view` rather than `basic_zstring_view` because the + * sub-range is generally not null-terminated. Callers who need a null-terminated tail should + * use the one-argument `substr(pos)` overload above. + * + * @param pos starting position. Must satisfy `pos <= size()`. + * @param count maximum number of characters in the resulting sub-range. Clamped to `size() - pos`. + * @throws std::out_of_range if `pos > size()` (propagated from `std::basic_string_view::substr`). + */ + WI_NODISCARD constexpr std::basic_string_view substr(size_type pos, size_type count) const + { + return std::basic_string_view(*this).substr(pos, count); + } + private: + // Trusted (non-validating) ctor used by substr() and other internal sites that have already + // established the invariants. + constexpr basic_zstring_view(_trusted_tag, const TChar* p, size_type n) noexcept : std::basic_string_view(p, n) + { + } + + // Self-guard helper for the explicit `std::basic_string_view` ctor and for the safe + // variant's `(ptr, len)` ctor. + static constexpr const TChar* _require_non_null(const TChar* p) noexcept + { + if (p == nullptr) + { + WI_STL_FAIL_FAST_IF(true); + } + return p; + } + // 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 std::basic_string_view view(str, buf_size); auto pos = view.find_first_of(TChar()); if (pos == view.npos) { @@ -235,17 +579,19 @@ 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 std::basic_string_view::swap; + using std::basic_string_view::remove_suffix; }; using zstring_view = basic_zstring_view; using zwstring_view = basic_zstring_view; +using zstring_view_safe = basic_zstring_view>; +using zwstring_view_safe = 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) +// This is the overload for basic_zstring_view (both variants). Other overloads available in resource.h. +template +inline auto str_raw_ptr(basic_zstring_view str) { return str.c_str(); } @@ -261,6 +607,16 @@ inline namespace literals { return {str, len}; } + + constexpr zstring_view_safe operator""_zvs(const char* str, std::size_t len) noexcept + { + return zstring_view_safe{str, len}; + } + + constexpr zwstring_view_safe operator""_zvs(const wchar_t* str, std::size_t len) noexcept + { + return zwstring_view_safe{str, len}; + } } // namespace literals #endif // __cpp_lib_string_view >= 201606L @@ -317,8 +673,8 @@ 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, TChar> { }; #endif diff --git a/tests/StlTests.cpp b/tests/StlTests.cpp index 4ae7678e..64cd2611 100644 --- a/tests/StlTests.cpp +++ b/tests/StlTests.cpp @@ -66,10 +66,10 @@ struct CustomNoncopyableString TEST_CASE("StlTests::TestZStringView", "[stl][zstring_view]") { - // Test empty cases + // A default-constructed nullable view is empty and has data() == nullptr (matches the prior + // single-class behaviour). The safe variant gets its own coverage in TestZStringViewSafe + // and in the templated matrix below. REQUIRE(wil::zstring_view{}.empty()); - REQUIRE(wil::zstring_view{}.data() == nullptr); - REQUIRE(wil::zstring_view{}.c_str() == nullptr); // Test empty string cases REQUIRE(wil::zstring_view{""}[0] == '\0'); @@ -173,10 +173,10 @@ TEST_CASE("StlTests::TestZStringView formatting", "[stl][zstring_view]") TEST_CASE("StlTests::TestZWStringView", "[stl][zstring_view]") { - // Test empty cases + // A default-constructed nullable view is empty and has data() == nullptr (matches the prior + // single-class behaviour). The safe variant gets its own coverage in TestZWStringViewSafe + // and in the templated matrix below. REQUIRE(wil::zwstring_view{}.empty()); - REQUIRE(wil::zwstring_view{}.data() == nullptr); - REQUIRE(wil::zwstring_view{}.c_str() == nullptr); // Test empty string cases REQUIRE(wil::zwstring_view{L""}[0] == L'\0'); @@ -245,4 +245,879 @@ TEST_CASE("StlTests::TestZWStringView", "[stl][zstring_view]") REQUIRE(wil::zwstring_view(fake_path) == L"hello"); } +// Regression test for the substr(0) round-trip on a default-constructed nullable view. The +// substr(pos) override returns a basic_zstring_view by routing tail.data()/tail.size() through +// the trusted (ptr, length) ctor, which would deref a null pointer if substr(0) on a +// default-constructed nullable view (data() == nullptr) reached that ctor. The short-circuit +// in substr's body fires for the tail.data() == nullptr case, so the tail is itself a +// default-constructed view. The safe variant cannot hit this case (its default-constructed +// data() points at the static empty buffer), so the regression is nullable-only. + +TEST_CASE("StlTests::TestZStringView default-constructed substr(0) round-trip is safe", "[stl][zstring_view]") +{ + wil::zstring_view zv; + auto tail = zv.substr(0); + REQUIRE(tail.empty()); + REQUIRE(tail.size() == 0); + REQUIRE(tail.data() == nullptr); + REQUIRE(tail.c_str() == nullptr); +} + +TEST_CASE("StlTests::TestZWStringView default-constructed substr(0) round-trip is safe", "[stl][zstring_view]") +{ + wil::zwstring_view zv; + auto tail = zv.substr(0); + REQUIRE(tail.empty()); + REQUIRE(tail.size() == 0); + REQUIRE(tail.data() == nullptr); + REQUIRE(tail.c_str() == nullptr); +} + +// Safe-variant default-construct contract. The safe variant (basic_zstring_view>, aliased as wil::zstring_view_safe / wil::zwstring_view_safe) +// guarantees `data() != nullptr` on a default-constructed view: the view points at a static +// empty buffer so c_str() is always dereferenceable. These cases exercise c_str() use patterns +// (strlen, %s, std::string ctor, C-API handoff), observable equivalence with an empty-literal +// view, and defensive `if (zv.data())` guards. The templated matrix below repeats the same +// contract for both variants under a single body via `if constexpr (is_safe_zsv_v)`. + +TEST_CASE("StlTests::TestZStringViewSafe default-construct safety", "[stl][zstring_view][safe]") +{ + SECTION("c_str()-style operations on a default-constructed view are well-defined") + { + wil::zstring_view_safe zv; + + // c_str() returns a dereferenceable pointer; first character is '\0'. + REQUIRE(zv.c_str() != nullptr); + REQUIRE(zv.c_str()[0] == '\0'); + REQUIRE(*zv.c_str() == '\0'); + + // strlen and equivalent C APIs see an empty string rather than UB. + REQUIRE(strlen(zv.c_str()) == 0); + + // Constructing a std::string from c_str() produces an empty string. + REQUIRE(std::string(zv.c_str()).empty()); + } + + SECTION("container-style operations on a default-constructed view") + { + wil::zstring_view_safe zv; + + REQUIRE(zv.empty()); + REQUIRE(zv.size() == 0); + REQUIRE(zv.length() == 0); + + // Decay to std::string_view via the safe variant's implicit conversion operator. + std::string_view sv = zv; + REQUIRE(sv.empty()); + REQUIRE(sv.size() == 0); + + // Self-equality and equality with an empty safe view. + REQUIRE(zv == wil::zstring_view_safe{}); + + // Iteration yields zero passes. + int count = 0; + for (auto c : zv) + { + (void)c; + ++count; + } + REQUIRE(count == 0); + } + + SECTION("default-constructed and empty-literal views are observably equivalent") + { + wil::zstring_view_safe defaulted; + wil::zstring_view_safe fromEmptyLiteral{""}; + + REQUIRE(defaulted.empty() == fromEmptyLiteral.empty()); + REQUIRE(defaulted.size() == fromEmptyLiteral.size()); + REQUIRE(strlen(defaulted.c_str()) == strlen(fromEmptyLiteral.c_str())); + REQUIRE(defaulted == fromEmptyLiteral); + } + + SECTION("defensive `if (zv.data())` guards compile and behave correctly") + { + wil::zstring_view_safe zv; + + // `if (zv.data())` is always true on a default-constructed safe view; the body + // operates on a valid empty C-string. + bool sawValidPointer = false; + if (zv.data()) + { + sawValidPointer = true; + REQUIRE(strlen(zv.c_str()) == 0); + } + REQUIRE(sawValidPointer); + + // `if (!zv.empty())` skips the body for any empty view. + bool sawNonEmpty = false; + if (!zv.empty()) + { + sawNonEmpty = true; + } + REQUIRE(!sawNonEmpty); + } +} + +TEST_CASE("StlTests::TestZWStringViewSafe default-construct safety", "[stl][zstring_view][safe]") +{ + SECTION("c_str()-style operations on a default-constructed view are well-defined") + { + wil::zwstring_view_safe zv; + + REQUIRE(zv.c_str() != nullptr); + REQUIRE(zv.c_str()[0] == L'\0'); + REQUIRE(*zv.c_str() == L'\0'); + + REQUIRE(wcslen(zv.c_str()) == 0); + + REQUIRE(std::wstring(zv.c_str()).empty()); + } + + SECTION("container-style operations on a default-constructed view") + { + wil::zwstring_view_safe zv; + + REQUIRE(zv.empty()); + REQUIRE(zv.size() == 0); + REQUIRE(zv.length() == 0); + + std::wstring_view sv = zv; + REQUIRE(sv.empty()); + REQUIRE(sv.size() == 0); + + REQUIRE(zv == wil::zwstring_view_safe{}); + + int count = 0; + for (auto c : zv) + { + (void)c; + ++count; + } + REQUIRE(count == 0); + } + + SECTION("default-constructed and empty-literal views are observably equivalent") + { + wil::zwstring_view_safe defaulted; + wil::zwstring_view_safe fromEmptyLiteral{L""}; + + REQUIRE(defaulted.empty() == fromEmptyLiteral.empty()); + REQUIRE(defaulted.size() == fromEmptyLiteral.size()); + REQUIRE(wcslen(defaulted.c_str()) == wcslen(fromEmptyLiteral.c_str())); + REQUIRE(defaulted == fromEmptyLiteral); + } + + SECTION("defensive `if (zv.data())` guards compile and behave correctly") + { + wil::zwstring_view_safe zv; + + bool sawValidPointer = false; + if (zv.data()) + { + sawValidPointer = true; + REQUIRE(wcslen(zv.c_str()) == 0); + } + REQUIRE(sawValidPointer); + + bool sawNonEmpty = false; + if (!zv.empty()) + { + sawNonEmpty = true; + } + REQUIRE(!sawNonEmpty); + } +} + +// Templated tests using Catch2's TEMPLATE_TEST_CASE to share the same body across char +// and wchar_t. Tagged [templated] so they can be filtered separately. Covers the +// null-termination invariant, the substr split, the contains backport, safe default-construct +// semantics, and the read-only base-class interface as seen through the derived type. + +namespace zsv_test_helpers +{ +template +struct strings; + +template <> +struct strings +{ + static constexpr const char* hello_world = "Hello, World!"; + static constexpr const char* hello = "Hello"; + static constexpr const char* world = "World!"; + static constexpr const char* empty = ""; + static constexpr const char* not_present = "xyz"; + static constexpr char test_char = 'o'; + static constexpr char absent_char = 'Z'; + // "abc" literal in each character type, used by the construction-matrix tests. + static constexpr const char* abc = "abc"; +}; + +template <> +struct strings +{ + static constexpr const wchar_t* hello_world = L"Hello, World!"; + static constexpr const wchar_t* hello = L"Hello"; + static constexpr const wchar_t* world = L"World!"; + static constexpr const wchar_t* empty = L""; + static constexpr const wchar_t* not_present = L"xyz"; + static constexpr wchar_t test_char = L'o'; + static constexpr wchar_t absent_char = L'Z'; + static constexpr const wchar_t* abc = L"abc"; +}; + +// Asserts the core null-termination invariant. Templated on the ZSV instantiation so it +// accepts both the nullable (`wil::zstring_view` / `wil::zwstring_view`) and the safe +// (`wil::zstring_view_safe` / `wil::zwstring_view_safe`) variants without an implicit +// cross-traits conversion at the call site. +template +void assert_null_terminated(ZSV v) +{ + using TChar = typename ZSV::value_type; + REQUIRE(v.data() != nullptr); + REQUIRE(v.data()[v.size()] == TChar()); +} + +// Distinguishes the safe Traits variant from the default-traits (nullable) variant by +// inspecting the ZSV's `traits_type`. The safe variant's traits is +// `wil::zstring_view_traits_safe` (derived from `std::char_traits` but a +// distinct type); the nullable variant uses `std::char_traits` directly. +template +constexpr bool is_safe_zsv_v = !std::is_same_v>; + +// Templated source type providing only c_str() (no size()) -- exercises the +// SFINAE constructor that accepts c_str()-only sources. +template +struct string_with_c_str_only +{ + using value_type = TChar; + constexpr const TChar* c_str() const + { + if constexpr (std::is_same_v) + { + return "hello"; + } + else + { + return L"hello"; + } + } +}; +} // namespace zsv_test_helpers + +#define ZSV_TYPES char, wchar_t + +// Four-way matrix: nullable + safe variants of both narrow and wide. Used by every templated +// test that should hold for either Traits choice. The two outliers that stay on `ZSV_TYPES` +// are the `_zv` literal and `std::format` blocks, because `_zv` always materialises the +// nullable variant (the safe-variant equivalent `_zvs` is covered by its own block). +#define ZSV_VARIANTS wil::zstring_view, wil::zwstring_view, wil::zstring_view_safe, wil::zwstring_view_safe + +TEMPLATE_TEST_CASE("StlTests::ZStringView construction", "[stl][zstring_view][templated]", ZSV_VARIANTS) +{ + using ZSV = TestType; + using TChar = typename ZSV::value_type; + using S = zsv_test_helpers::strings; + + SECTION("default and empty-literal views are both empty and read as null-terminated") + { + // Default-construct: both variants report empty(); the safe variant additionally + // guarantees a dereferenceable null-terminated buffer, while the nullable variant + // keeps the legacy nullptr-data behaviour. + REQUIRE(ZSV{}.empty()); + if constexpr (zsv_test_helpers::is_safe_zsv_v) + { + REQUIRE(ZSV{}.data() != nullptr); + REQUIRE(ZSV{}.c_str()[0] == TChar()); + } + else + { + REQUIRE(ZSV{}.data() == nullptr); + // c_str() is a thin alias for data() on the default variant; assert both to pin + // down the contract in case c_str() ever grows independent logic. + REQUIRE(ZSV{}.c_str() == nullptr); + } + // Empty literal: subscript reads '\0', c_str() reads '\0', empty(). + REQUIRE(ZSV{S::empty}[0] == TChar()); + REQUIRE(ZSV{S::empty}.c_str()[0] == TChar()); + REQUIRE(ZSV{S::empty}.empty()); + } + + SECTION("all ctor paths produce equivalent views") + { + // ctor from string literal (and length matches char_traits::length). + ZSV fromLiteral = S::abc; + REQUIRE(fromLiteral.length() == std::char_traits::length(S::abc)); + + // ctor from std::basic_string. + std::basic_string stlString = S::abc; + ZSV fromString(stlString); + // ctor from TChar* (pointer). + ZSV fromPtr(stlString.data()); + // ctor from null-terminated array. + static constexpr TChar charArray[] = {TChar('a'), TChar('b'), TChar('c'), TChar()}; + ZSV fromArray(charArray); + // ctor from extended array (trailing zeros beyond the first null). + static constexpr TChar extendedCharArray[] = {TChar('a'), TChar('b'), TChar('c'), TChar(), TChar(), TChar(), TChar(), TChar()}; + ZSV fromExtendedArray(extendedCharArray); + // copy ctor. + ZSV copy = fromLiteral; + + // The mixed-traits comparison (basic_string_view vs + // basic_string) has no matching operator==; route both sides + // through std::basic_string_view so the nullable variant slices to its + // inherited base and the safe variant exercises its implicit conversion operator. + REQUIRE(std::basic_string_view{fromLiteral} == stlString); + REQUIRE(fromLiteral == fromString); + REQUIRE(fromLiteral == fromPtr); + REQUIRE(fromLiteral == fromArray); + REQUIRE(fromLiteral == fromExtendedArray); + REQUIRE(fromLiteral == copy); + } + + SECTION("decays to base string_view") + { + ZSV fromLiteral = S::abc; + std::basic_string_view view = fromLiteral; + // Mixed-traits compare: see note in "all ctor paths produce equivalent views". + REQUIRE(view == std::basic_string_view{fromLiteral}); + } + + SECTION("operator[] reads each char including the trailing null") + { + ZSV fromLiteral = S::abc; + REQUIRE(fromLiteral[0] == TChar('a')); + REQUIRE(fromLiteral[1] == TChar('b')); + REQUIRE(fromLiteral[2] == TChar('c')); + // WIL's operator[] override allows reading view[size()] (the null terminator), + // unlike base basic_string_view which forbids it. + REQUIRE(fromLiteral[3] == TChar()); + } + + SECTION("fail-fast on no-NULL-in-range for explicit-length and array ctors") + { + static constexpr TChar badCharArray[2][3] = {{TChar('a'), TChar('b'), TChar('c')}, {TChar('a'), TChar('b'), TChar('c')}}; + REQUIRE_ERROR((ZSV{&badCharArray[0][0], _countof(badCharArray[0])})); + REQUIRE_ERROR((ZSV{badCharArray[0]})); + } + + SECTION("explicit-length ctor accepts the off-by-one valid boundary case") + { + ZSV fromLiteral = S::abc; + static constexpr TChar badCharArrayOffByOne[2][3] = {{TChar('a'), TChar('b'), TChar('c')}, {}}; + const ZSV fromTerminatedCharArray(&badCharArrayOffByOne[0][0], _countof(badCharArrayOffByOne[0])); + REQUIRE(fromLiteral == fromTerminatedCharArray); + REQUIRE_ERROR((ZSV{badCharArrayOffByOne[0]})); + } + + SECTION("ctor from a type implicitly convertible to a C-string pointer") + { + CustomNoncopyableString customString; + ZSV fromCustomString(customString); + if constexpr (std::is_same_v) + { + REQUIRE(fromCustomString == static_cast(customString)); + } + else + { + REQUIRE(fromCustomString == static_cast(customString)); + } + } + + SECTION("ctor from a type with only c_str() (no size())") + { + zsv_test_helpers::string_with_c_str_only fake_path{}; + ZSV view(fake_path); + if constexpr (std::is_same_v) + { + REQUIRE(view == "hello"); + } + else + { + REQUIRE(view == L"hello"); + } + } +} + +TEMPLATE_TEST_CASE("StlTests::ZStringView _zv literal", "[stl][zstring_view][templated]", ZSV_TYPES) +{ + using TChar = TestType; + + SECTION("_zv literal preserves length and content") + { + if constexpr (std::is_same_v) + { + auto str = "Hello, world!"_zv; + STATIC_REQUIRE(std::is_same_v); + REQUIRE(str.length() == 13); + REQUIRE(str[0] == 'H'); + REQUIRE(str[12] == '!'); + } + else + { + auto str = L"Hello, world!"_zv; + STATIC_REQUIRE(std::is_same_v); + REQUIRE(str.length() == 13); + REQUIRE(str[0] == L'H'); + REQUIRE(str[12] == L'!'); + } + } +} + +// Parity block for the safe-variant literal. Mirrors the `_zv literal` test above so we keep +// the matrix-style coverage symmetric across both UDLs; the decltype assertion is the +// load-bearing piece - it pins down which alias the literal materialises. +TEMPLATE_TEST_CASE("StlTests::ZStringView _zvs literal", "[stl][zstring_view][templated]", ZSV_TYPES) +{ + using TChar = TestType; + + SECTION("_zvs literal preserves length and content and materialises the safe variant") + { + if constexpr (std::is_same_v) + { + auto str = "Hello, world!"_zvs; + STATIC_REQUIRE(std::is_same_v); + REQUIRE(str.length() == 13); + REQUIRE(str[0] == 'H'); + REQUIRE(str[12] == '!'); + } + else + { + auto str = L"Hello, world!"_zvs; + STATIC_REQUIRE(std::is_same_v); + REQUIRE(str.length() == 13); + REQUIRE(str[0] == L'H'); + REQUIRE(str[12] == L'!'); + } + } +} + +#if __cpp_lib_format >= 201907L +TEMPLATE_TEST_CASE("StlTests::ZStringView std::format support", "[stl][zstring_view][templated]", ZSV_TYPES) +{ + using TChar = TestType; + + SECTION("round-trips through std::format") + { + if constexpr (std::is_same_v) + { + auto str = "kittens"_zv; + auto fmtStr = std::format("Hello {}", str); + REQUIRE(fmtStr == "Hello kittens"); + } + else + { + auto str = L"kittens"_zv; + auto fmtStr = std::format(L"Hello {}", str); + REQUIRE(fmtStr == L"Hello kittens"); + } + } +} +#endif // __cpp_lib_format >= 201907L + +TEST_CASE("StlTests::ZStringView type aliases", "[stl][zstring_view]") +{ + // Nullable variant: default-traits aliases and `_zv` UDL bindings. + STATIC_REQUIRE(std::is_same_v>); + STATIC_REQUIRE(std::is_same_v>); + STATIC_REQUIRE(std::is_same_v); + STATIC_REQUIRE(std::is_same_v); + + // Safe variant: opt-in Traits aliases and `_zvs` UDL bindings. + STATIC_REQUIRE(std::is_same_v>>); + STATIC_REQUIRE(std::is_same_v>>); + STATIC_REQUIRE(std::is_same_v); + STATIC_REQUIRE(std::is_same_v); + + // The two variants are distinct types: the Traits parameter changes the instantiation, so + // there is no implicit ABI/identity collapse between safe and nullable. + STATIC_REQUIRE(!std::is_same_v); + STATIC_REQUIRE(!std::is_same_v); +} + +// Pins down the behavioural delta between the safe and nullable variants in one place. +// Anything that *should* differ between the two Traits choices lives here, so a future change +// that accidentally collapses the two will trip a focused, well-named failure. +TEST_CASE("StlTests::ZStringView safe-variant invariants", "[stl][zstring_view][safe]") +{ + SECTION("safe variant rejects nullptr_t construction at compile time") + { + // The safe variant SFINAE-deletes its `nullptr_t` constructor; the nullable variant + // accepts `nullptr` via its inherited base. Both are intentional. + STATIC_REQUIRE(!std::is_constructible_v); + STATIC_REQUIRE(!std::is_constructible_v); + STATIC_REQUIRE(std::is_constructible_v); + STATIC_REQUIRE(std::is_constructible_v); + } + + SECTION("safe variant fail-fasts on (nullptr, 0) at runtime") + { + // The safe variant's `(ptr, len)` constructor routes through `_require_non_null` and + // fail-fasts when the pointer is null, *even* for zero-length. The nullable variant + // is deliberately not exercised here: its `(ptr, len)` ctor unconditionally reads + // `pStringData[stringLength]` and therefore has latent UB when called with + // `(nullptr, 0)`. Documenting that gap rather than triggering it. + REQUIRE_ERROR((wil::zstring_view_safe{nullptr, 0})); + REQUIRE_ERROR((wil::zwstring_view_safe{nullptr, 0})); + } + + SECTION("safe variant's default-constructed view has a non-null buffer") + { + // The whole point of the safe Traits: the default-constructed view dereferences to a + // valid empty C-string instead of nullptr. Mirrors the templated `construction` block + // but kept here as an at-a-glance, narrative-style guarantee. + constexpr wil::zstring_view_safe narrow; + constexpr wil::zwstring_view_safe wide; + STATIC_REQUIRE(narrow.data() != nullptr); + STATIC_REQUIRE(wide.data() != nullptr); + STATIC_REQUIRE(narrow.empty()); + STATIC_REQUIRE(wide.empty()); + REQUIRE(narrow.c_str()[0] == '\0'); + REQUIRE(wide.c_str()[0] == L'\0'); + } +} + +TEMPLATE_TEST_CASE("StlTests::ZStringView element access (data/c_str identity)", "[stl][zstring_view][templated]", ZSV_VARIANTS) +{ + using ZSV = TestType; + using TChar = typename ZSV::value_type; + using S = zsv_test_helpers::strings; + + ZSV zv = S::hello_world; + + SECTION("data() and c_str() return the same pointer and are null-terminated") + { + REQUIRE(zv.data() == zv.c_str()); + REQUIRE(zv.c_str()[zv.size()] == TChar()); + } +} + +#if defined(__cpp_lib_starts_ends_with) && __cpp_lib_starts_ends_with >= 201711L +TEMPLATE_TEST_CASE("StlTests::ZStringView starts_with and ends_with", "[stl][zstring_view][templated]", ZSV_VARIANTS) +{ + using ZSV = TestType; + using TChar = typename ZSV::value_type; + using S = zsv_test_helpers::strings; + + ZSV zv = S::hello_world; + + SECTION("starts_with matches a known prefix") + { + REQUIRE(zv.starts_with(S::hello)); + REQUIRE(!zv.starts_with(S::world)); + REQUIRE(zv.starts_with(S::hello_world[0])); // single char + } + + SECTION("ends_with matches a known suffix") + { + REQUIRE(zv.ends_with(S::world)); + REQUIRE(!zv.ends_with(S::hello)); + } + + SECTION("empty pattern always starts_with and ends_with anything") + { + REQUIRE(zv.starts_with(S::empty)); + REQUIRE(zv.ends_with(S::empty)); + } +} +#endif // __cpp_lib_starts_ends_with + +TEMPLATE_TEST_CASE("StlTests::ZStringView contains", "[stl][zstring_view][templated]", ZSV_VARIANTS) +{ + using ZSV = TestType; + using TChar = typename ZSV::value_type; + using S = zsv_test_helpers::strings; + + ZSV zv = S::hello_world; + + SECTION("contains(string_view) finds embedded substrings") + { + REQUIRE(zv.contains(ZSV(S::hello))); + REQUIRE(zv.contains(ZSV(S::world))); + REQUIRE(!zv.contains(ZSV(S::not_present))); + } + + SECTION("contains(char) finds a present character") + { + REQUIRE(zv.contains(S::test_char)); + REQUIRE(!zv.contains(S::absent_char)); + } + + SECTION("contains(const TChar*) finds embedded substrings via raw pointer") + { + REQUIRE(zv.contains(S::hello)); + REQUIRE(!zv.contains(S::not_present)); + } + + SECTION("empty view contains nothing except the empty substring") + { + ZSV empty; + REQUIRE(!empty.contains(S::test_char)); + REQUIRE(!empty.contains(S::hello)); + REQUIRE(empty.contains(ZSV())); + } + + SECTION("contains return type is bool") + { + STATIC_REQUIRE(std::is_same_v); + STATIC_REQUIRE(std::is_same_v); + STATIC_REQUIRE(std::is_same_v); + } +} + +TEMPLATE_TEST_CASE("StlTests::ZStringView substr(pos) is covariant and preserves null-termination", "[stl][zstring_view][templated][substr]", ZSV_VARIANTS) +{ + using ZSV = TestType; + using TChar = typename ZSV::value_type; + using S = zsv_test_helpers::strings; + + ZSV zv = S::hello_world; + const std::size_t n = std::char_traits::length(S::hello_world); + + SECTION("substr(pos) returns a basic_zstring_view (covariant return type)") + { + STATIC_REQUIRE(std::is_same_v); + STATIC_REQUIRE(std::is_same_v); + } + + SECTION("substr(pos) returns the null-terminated tail") + { + auto tail = zv.substr(7); + REQUIRE(tail.size() == n - 7); + REQUIRE(tail == ZSV(S::world)); + zsv_test_helpers::assert_null_terminated(tail); + } + + SECTION("substr(0) returns a copy of the whole view") + { + auto whole = zv.substr(0); + REQUIRE(whole == zv); + zsv_test_helpers::assert_null_terminated(whole); + } + + SECTION("substr(size()) returns an empty view at the trailing null") + { + auto empty = zv.substr(n); + REQUIRE(empty.empty()); + REQUIRE(empty.size() == 0); + zsv_test_helpers::assert_null_terminated(empty); + } + + SECTION("substr(pos > size()) throws out_of_range, propagating from base") + { + REQUIRE_THROWS_AS(zv.substr(n + 1), std::out_of_range); + } + + SECTION("substr chaining preserves null-termination at each step") + { + auto step1 = zv.substr(0); // "Hello, World!" + auto step2 = step1.substr(7); // "World!" + auto step3 = step2.substr(0); // "World!" + zsv_test_helpers::assert_null_terminated(step1); + zsv_test_helpers::assert_null_terminated(step2); + zsv_test_helpers::assert_null_terminated(step3); + REQUIRE(step3 == ZSV(S::world)); + } +} + +TEMPLATE_TEST_CASE("StlTests::ZStringView substr(pos, count) returns string_view", "[stl][zstring_view][templated][substr]", ZSV_VARIANTS) +{ + using ZSV = TestType; + using TChar = typename ZSV::value_type; + // `substr(pos, count)` is the inherited base override, so it returns the base's + // own-traits string_view -- not necessarily `std::basic_string_view` with default + // traits. For the safe variant the base is `basic_string_view`. + using SV = std::basic_string_view; + using S = zsv_test_helpers::strings; + + ZSV zv = S::hello_world; + + SECTION("substr(pos, count) returns a std::basic_string_view, not a basic_zstring_view") + { + STATIC_REQUIRE(std::is_same_v); + STATIC_REQUIRE(std::is_same_v); + } + + SECTION("substr(0, prefix_len) returns the prefix as a string_view") + { + auto prefix = zv.substr(0, 5); + REQUIRE(prefix.size() == 5); + REQUIRE(prefix == SV(S::hello)); + } + + SECTION("substr(pos, count) clamps count to size() - pos") + { + auto slice = zv.substr(7, 1000); + REQUIRE(slice.size() == std::char_traits::length(S::hello_world) - 7); + } + + SECTION("substr(pos, 0) returns an empty string_view") + { + auto empty = zv.substr(3, 0); + REQUIRE(empty.empty()); + REQUIRE(empty.size() == 0); + } + + SECTION("substr(size(), count) returns an empty string_view at the trailing null") + { + const auto n = zv.size(); + auto empty = zv.substr(n, 5); + REQUIRE(empty.empty()); + } +} + +TEMPLATE_TEST_CASE("StlTests::ZStringView remove_prefix preserves null-termination", "[stl][zstring_view][templated]", ZSV_VARIANTS) +{ + using ZSV = TestType; + using TChar = typename ZSV::value_type; + using S = zsv_test_helpers::strings; + + SECTION("after remove_prefix the view remains null-terminated and c_str() == data()") + { + ZSV zv = S::hello_world; + zv.remove_prefix(7); + zsv_test_helpers::assert_null_terminated(zv); + REQUIRE(zv.c_str() == zv.data()); + REQUIRE(zv.c_str()[zv.size()] == TChar()); + } +} + +TEMPLATE_TEST_CASE("StlTests::ZStringView conversion to string_view", "[stl][zstring_view][templated]", ZSV_VARIANTS) +{ + using ZSV = TestType; + using TChar = typename ZSV::value_type; + // Deliberately default-traits `std::basic_string_view` here: the test asserts both + // variants interoperate with the broad ecosystem of APIs that take the stdlib's + // default-traits string_view. The nullable variant gets this via inherited-base + // slicing; the safe variant uses its implicit `operator std::basic_string_view()`. + using SV = std::basic_string_view; + using S = zsv_test_helpers::strings; + + SECTION("implicit conversion to string_view preserves size and data") + { + ZSV zv = S::hello_world; + SV sv = zv; + REQUIRE(sv.size() == zv.size()); + REQUIRE(sv.data() == zv.data()); + } + + SECTION("can pass a basic_zstring_view to a function expecting basic_string_view") + { + auto take_sv = [](SV sv) { + return sv.size(); + }; + ZSV zv = S::hello_world; + REQUIRE(take_sv(zv) == zv.size()); + } + + SECTION("can construct a std::basic_string from a basic_zstring_view") + { + ZSV zv = S::hello_world; + std::basic_string str(zv); + REQUIRE(str.size() == zv.size()); + // Mixed-traits compare: route through default-traits string_view so the safe variant's + // basic_string_view doesn't break the inherited operator== match. + REQUIRE(str == std::basic_string_view{zv}); + } +} + +TEMPLATE_TEST_CASE("StlTests::ZStringView cross-variant constructors preserve data and size", "[stl][zstring_view][templated]", ZSV_TYPES) +{ + using TChar = TestType; + using Safe = wil::basic_zstring_view>; + using Default = wil::basic_zstring_view; + using S = zsv_test_helpers::strings; + + SECTION("implicit ctor: a non-empty safe view widens to a default view sharing data and size") + { + Safe safe = S::hello_world; + Default def = safe; + REQUIRE(def.data() == safe.data()); + REQUIRE(def.size() == safe.size()); + REQUIRE(def.c_str() == safe.c_str()); + } + + SECTION("implicit ctor: a default-constructed safe view widens to a default view with non-null data") + { + Safe safe; + Default def = safe; + REQUIRE(def.data() != nullptr); + REQUIRE(def.size() == 0); + REQUIRE(def.empty()); + } + + SECTION("explicit ctor: an engaged default view narrows to a safe view sharing data and size") + { + Default def = S::hello_world; + Safe safe{def}; + REQUIRE(safe.data() == def.data()); + REQUIRE(safe.size() == def.size()); + } +} + +TEMPLATE_TEST_CASE("StlTests::ZStringView safe-variant disambiguates between zstring_view and string_view overloads", "[stl][zstring_view][templated]", ZSV_TYPES) +{ + using TChar = TestType; + using Safe = wil::basic_zstring_view>; + using S = zsv_test_helpers::strings; + + // Pin down the disambiguation idiom from the class doc-block's overload-resolution caveat. + // A call site that has both `f(wil::zstring_view)` and `f(std::string_view)` overloads cannot + // accept a `zstring_view_safe` directly (ambiguous: both are single user-defined conversions), + // but either explicit wrap form resolves cleanly. + + enum class which + { + default_variant, + default_string_view, + }; + + struct overloads + { + static which f(wil::basic_zstring_view) noexcept + { + return which::default_variant; + } + static which f(std::basic_string_view) noexcept + { + return which::default_string_view; + } + }; + + Safe safe = S::hello_world; + + REQUIRE(overloads::f(wil::basic_zstring_view{safe}) == which::default_variant); + REQUIRE(overloads::f(std::basic_string_view{safe}) == which::default_string_view); +} + +TEMPLATE_TEST_CASE("StlTests::ZStringView constexpr usage", "[stl][zstring_view][templated]", ZSV_VARIANTS) +{ + using ZSV = TestType; + using TChar = typename ZSV::value_type; + using S = zsv_test_helpers::strings; + + SECTION("default ctor, copy ctor, and basic accessors are constexpr-usable") + { + constexpr ZSV defaulted; + STATIC_REQUIRE(defaulted.empty()); + STATIC_REQUIRE(defaulted.size() == 0); + if constexpr (zsv_test_helpers::is_safe_zsv_v) + { + STATIC_REQUIRE(defaulted.data() != nullptr); + } + else + { + STATIC_REQUIRE(defaulted.data() == nullptr); + } + } + + SECTION("substr(pos) is constexpr-usable when constructed from a literal") + { + constexpr ZSV zv = S::hello_world; + constexpr auto tail = zv.substr(7); + STATIC_REQUIRE(tail.size() == std::char_traits::length(S::hello_world) - 7); + } +} + #endif