Skip to content

basic_zstring_view: add opt-in nonnull variants - #668

Draft
Monroe Thomas (mmthomas) wants to merge 4 commits into
microsoft:masterfrom
mmthomas:users/mmthomas/nonnull-zstring-view
Draft

basic_zstring_view: add opt-in nonnull variants#668
Monroe Thomas (mmthomas) wants to merge 4 commits into
microsoft:masterfrom
mmthomas:users/mmthomas/nonnull-zstring-view

Conversation

@mmthomas

@mmthomas Monroe Thomas (mmthomas) commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

Summary

wil::zstring_view is a non-owning view of a null-terminated string. Its default constructor follows std::string_view: the view is empty and data() is null. That behavior must remain unchanged because existing callers may use null to mean "no string."

Some callers instead need an empty view that can be passed directly to a C API without first checking for null. This PR adds opt-in nonnull_zstring_view and nonnull_zwstring_view aliases for that use case. Their constructors reject null pointers, and their default constructors point at an internal empty string.

wil::zstring_view nullable;            // data() == nullptr
wil::nonnull_zstring_view nonnull;     // data() != nullptr, c_str()[0] == '\0'

printf("%s", nonnull.c_str());         // safe without a null check

The existing zstring_view and zwstring_view aliases retain their current behavior.

C++ standardization

WG21 is standardizing the same general abstraction for C++29 as std::basic_cstring_view in P3655R5, std::cstring_view. The proposed type is a non-owning view of a null-terminated string. Its default constructor refers to a static null terminator, so data() and c_str() return a valid empty string rather than null, and direct nullptr construction is deleted.

P3655 is an active proposal rather than part of the published C++ standard. WIL retains its existing zstring_view naming and public-inheritance design; this PR adds an opt-in construction policy that provides the proposal's non-null empty-state behavior without changing existing callers.

Public API

template <typename TChar, typename Traits = std::char_traits<TChar>>
struct nonnull_zstring_view_traits
{
    using char_traits = Traits;
    static constexpr bool empty_strings_are_non_null = true;
};

template <class TChar, class Traits = std::char_traits<TChar>>
class basic_zstring_view;

using nonnull_zstring_view =
    basic_zstring_view<char, nonnull_zstring_view_traits<char>>;
using nonnull_zwstring_view =
    basic_zstring_view<wchar_t, nonnull_zstring_view_traits<wchar_t>>;

The policy type keeps the non-null behavior separate from the character traits used by std::basic_string_view. As a result, nonnull_zstring_view and zstring_view both derive from std::string_view, rather than deriving from different std::basic_string_view specializations.

This matters for normal C++ interoperability:

wil::nonnull_zstring_view value{"hello"};
std::string_view& base = value; // binds to the inherited base object

Custom character traits remain supported through nonnull_zstring_view_traits<TChar, Traits>.

Construction and conversion behavior

  • Default construction produces an empty view backed by an internal null terminator.
  • Direct nullptr construction is deleted.
  • Other null pointer inputs report a contract violation through WIL's existing fail-fast mechanism.
  • String literals, std::basic_string, compatible string-like objects, and valid pointer inputs behave like the existing type.
  • Conversion from a non-null view to the existing nullable view is implicit because it weakens the guarantee.
  • Conversion from a nullable view to a non-null view is explicit and checks the source pointer.
  • substr(pos) preserves the selected policy. A substring of a default-constructed non-null view therefore remains non-null.
  • Object size and trivial copyability are unchanged.

Inheritance limitation

basic_zstring_view publicly inherits from std::basic_string_view. This permits a caller to explicitly obtain a mutable base reference and assign a nullable base view:

wil::nonnull_zstring_view value{"hello"};
std::string_view& base = value;
base = std::string_view{}; // bypasses the non-null construction policy

The new type enforces non-null construction through its own API. In debug builds, the derived c_str() asserts if base-class mutation has changed the stored pointer to null. Calls made directly through the base class still bypass that check. Removing the escape hatch entirely would require replacing the existing inheritance design rather than extending it.

Compatibility

Existing code can continue to use wil::zstring_view and wil::zwstring_view with the same source syntax and nullable default behavior. Adding the defaulted Traits parameter changes the compiler-generated linker name for functions that expose basic_zstring_view in a binary interface. Default construction also now runs the policy-selection constructor instead of being a trivial operation. Object layout, size, and trivial copyability remain unchanged.

Tests

The focused tests cover both char and wchar_t variants:

  • nullable and non-null default construction;
  • deleted and fail-fast null construction paths;
  • implicit and explicit cross-variant conversions;
  • mutable std::basic_string_view base-reference compatibility;
  • preservation through substr(pos);
  • custom underlying character traits;
  • str_raw_ptr and std::format integration;
  • size and trivial-copyability checks.

Local validation:

  • MSVC Debug C++17 witest.exe "[zstring_view]": 155 assertions passed.
  • MSVC Debug C++23 witest.cpplatest.exe "[zstring_view]": 159 assertions passed.
  • Both builds completed without compiler warnings.

Scope

This PR does not add new string literals, change SAL annotations, alter the existing nullable aliases, or redesign basic_zstring_view to remove public inheritance.

The API direction originated in the compatibility and traits discussion on #635; this description is intended to stand on its own.

Monroe Thomas added 4 commits August 19, 2026 13:45
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
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
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
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant