---
title: "Design goals for zstring_view"
document: P4220R1
date: 2026-05-23
audience: LEWG
reply-to:
  - "Andrzej Krzemieński <akrzemi1@gmail.com>"
---

During the 2025 Sofia meeting, LEWG declared consensus to spend more time on `zstring_view` ([[P3655R4]](https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2026/p3655r4.html)). This paper follows the direction. We want to make sure that the design goals for `zstring_view` are clearly understood before any decisions whether to even have it, or in what form, are made.

## Revision history

### R1

1. Shown another example of a common implementation of a class named `string_view` with different goals and design decisions than those in [[P3655R4]](https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2026/p3655r4.html).
2. Provided an example illustrating situations where even the presence of `string_view` requires an allocation in order to talk to C APIs.
3. Acknowledged [[P4227R0]](https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2026/p4227r0.pdf).

## Terminology

### Contract

In this paper, whenever the word 'contract' is used, we *do not* refer to the C++26 feature known as "contract assertions", but instead refer to the methods of Library API specifications.

### C-string contract

The term *C-string contract* refers to the convention that the C programming language uses for representing strings:

1. The type is `const char*`
2. The pointer is not null.
3. The pointer points to a valid array of characters.
4. The iteration over the array is guaranteed to reach the zero character without hitting any undefined behavior. The position of this zero character determines the string size.
5. A zero-size string is a valid string value.

Note that under this contract it is impossible for the string to have a zero character within its contents, as — per the contract — the first occurrence of this character indicates the one-past-last character.

Note also that in the C library there are functions that take

- (`const char*`, `size_t`), like `strnlen`, where the size of the string is determined as the smaller of "the distance from the closest zero character" and the explicit length;
- (`const void*`, `size_t`), like `fwrite`, which can in particular work for arrays of `char` and where the length is provided explicitly and zero characters can appear in the middle.

They are excluded from the definition of the *C-string contract*.

## The motivation

A proposal should set out with a very clear goal statement, so that LEWG can evaluate:

- if the goal is worth pursuing;
- if the proposed solution addresses the stated goal optimally, or at all.

"A lot of people ask for it" or "there is a lot of GitHub libraries using the same class name" or "`std` does not yet have a type with this combination of properties" is not a sufficient motivation.

A lot of people ask that C++ provides a `finally` keyword. The correct response in that case has been to educate people on C++'s destructors rather than just fulfilling the request.

Similarly, a lot of people may demand "a type like `cstring_view`", but each of them may mean slightly different incompatible semantics, addressing slightly different incompatible goals.

The motivation that we have identified in [[P3655R4]](https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2026/p3655r4.html) is the experience where:

1. The author of the function will be calling a system function, such as POSIX [`open`](https://www.man7.org/linux/man-pages/man2/openat.2.html).
2. The author insists on using `std::string_view` as the function parameter type rather than `const char*`.

Such setup "compiles" and could work, but the callers would need to be informed and then disciplined to only create `string_view` objects that happen to be zero-terminated. This is called a *precondition*. Other options that the author has are to either use `const char*` as the function parameter type along with the C-string contract, or introduce a new type that directly reflects the C-string contract. In all three cases, the fact that a pointer to a zero-terminated character array will be used in the implementation is exposed in the function's contract, either as a precondition or as a dedicated type (because if it weren't, we would have just used `string_view`).

So, given that the C-string contract will be used anyway, why not just use `const char*` as the function parameter type, rather than insisting on a new type? The answers could be:

1. To reflect in the type the intention is to represent the C-string contract. We will call this goal "type-tagged `const char*`".
2. To enable the type, if so configured, to runtime-enforce the C-string contract (even at the cost of making the interface less convenient and exposing worse algorithmic complexity). We will call this goal "C-string with runtime contract enforcement".
3. To avoid the misleading semantics of `operator==` for `const char*` with the C-string contract. This does not become a distinct goal — it is served by all other mentioned goals.
4. To offer a type with the C-string contract which additionally exposes as much of the `string_view` interface as possible, including keeping the length explicitly, and accessing it in 𝒪(1). We will call this goal "`string_view` with `c_str()`".

Let's illustrate the last bullet. This would be in the situation where a program receives a zero-terminated string from one C-style API, then plays with it using the `string_view` interface, and finally passes it to another C-style API that uses the C-string contract:

```
void demonstration()
{
  const char * s = clib1::get_str();     // #1
  play1(string_view(s).starts_with("pre"));
  play2(string_view(s).find_last_not_of('_'));
  clib2::use_str(s); 
}
```

If the string size could be computed in line #1, then in lines #2 and #3 we could use it for free.

Ultimately, however, the `const char*` will be passed to the system function [`open`](https://www.man7.org/linux/man-pages/man2/openat.2.html) which will not care whether we know the string size or not: it will unconditionally iterate over the array until the zero terminator anyway. No design in `zstring_view` can change that.

## Design decisions

The goal of this section is to illustrate two things:

1. The evaluation of the design aspects of `zstring_view` can change from "correct" to "incorrect" depending on what the declared goal for the type is.
2. Two (or more) popular goals for a `zstring_view`-like class cannot be pursued simultaneously with one type without critical compromises, as the design decisions required will compromise one or the other goal.

### Constructors

The C-string semantic contract consists of three parts.

1. Checking if the pointer is non-null is trivial.
2. Checking if the pointer points to a valid character array is impossible.
3. Checking for a zero character is tricky: we could iterate over the elements to look for it, but if it isn't there we will reach UB.

If the primary goal of `zstring_view` is to runtime-enforce the C-string contract, then the whole point is to be able to test #3 above. This is doable if in the constructors we are additionally provided the limit for the iteration. It is easy to do for some constructors:

1. `zstring_view(string const& s)` — `s` already guarantees the null terminator.
2. `zstring_view(const char(&a)[N])` — `N` (a template parameter) is the limit, and we can check for zero at `N - 1`.
3. Copy/move constructor — it is safe to assume that we already performed the check when creating the original.

But we definitely cannot accept `zstring_view(const char*)`, as proposed in [[P3655R4]](https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2026/p3655r4.html), because we will not be able to verify the contract.

It is definitely a popular need to create a `zstring_view` having only a `const char*` obtained from another API:

```
catch(std::exception const& exc)
{
  string s = exc.what();        // ok
  string_view sv = exc.what();  // ok
  zstring_view zv = exc.what(); // do what?
}
```

For the goal "C-string with runtime contract enforcement", options are extremely limited. Simply allowing the conversion compromises the goal. Simply disallowing construction from `const char*` renders the type unusable in many common cases.

If we only provide constructor `zstring_view(const char*, size_t)` instead, we either make the usage of this type impossible, or bug prone:

```
catch(std::exception const& exc)
{
  size_t Max = 128;  // arbitrary size limit
  zstring_view zv(exc.what(), Max); // compiles, but may be UB
}
```

We could consider a slightly different goal instead, and say that the new type *either* allows a runtime-verifiable correct construction *or* provides a very explicit syntax for uncheckable initialization that is easy to audit:

```
catch (std::exception const& exc)
{
  auto zv = zstring_view::RISKY_convert(exc.what()); // ok
}
```

Such `zstring_view` would still be far from being a drop-in replacement for `const char*` in function parameters.

In contrast, if the goal is "`string_view` with `c_str()`" or "type-tagged `const char*`", then the conversion from `const char*` is fine, but we cannot provide constructors that delimit the size of the string by means other than zero character:

1. `zstring_view(iterator begin, iterator end)`.
2. `zstring_view(const char* str, size_type len)`.

This has an interesting consequence for the following case, currently handled by `std::string_view`:

```
void process(string const& json_rq)
{
  string_view filename = read_property(json_rq, "config.filename"); // (1) obviously not zero-terminated
  security_validation(filename);
  fopen(string(filename).c_str());  // (3) unfortunate copy
}
```

Avoiding the unfortunate copy in (3) is actually part of the goal "`string_view` with `c_str()`". But if we changed the type of `filename` from `string_view` to `zstring_view` we haven't solved the problem. The file name in the JSON request surely does not sit at the end, so if we want to extract it without making a copy, we will need to indicate the end of the sequence by means other than the zero character. C++ may end up in the situation where it has the `zstring_view` but the users still need to do allocations in order to call C APIs.

Depending on what the goal of `zstring_view` is, a different set of constructors may be optimal.

### The richness of the interface

If the goal is "C-string with runtime contract enforcement" or "Type-tagged `const char*`", then the excessive richness of the `std::string` interface (such as function `find_last_not_of`) is not necessary.

Compare the different contracts of `string`, `string_view` and `zstring_view`.

1. `string` — a `char` container that additionally exposes the string-rich interface. Zero is a perfectly valid element value.
2. `string_view` — an arbitrary sub-sequence of another `char` sequence managed elsewhere, which also exposes string-rich interface. Zero is a perfectly valid element value.
3. `zstring_view` — a `const char*` which can additionally runtime-enforce the C-string contract. By contract definition, it cannot have zero characters in the middle, and it will only be passed to function [`open`](https://www.man7.org/linux/man-pages/man2/openat.2.html).

The only interface of `zstring_view` that will be used in practice is its constructors and function `.c_str()`. Even `operator[]` is unnecessary: just call `.c_str()` and iterate over this array.

We lose the string-rich interface, but do we need it? If so, we can convert to `string_view`. This would be an 𝒪(n) cost if the goal is "C-string contract enforcer", or an 𝒪(1) cost if the goal is "C-string contract + precomputed size". In the latter case we would penalize the most basic use case:

```
zstring_view zs = get_c_string(); // 𝒪(n) pass
open(zs.c_str());         // another 𝒪(n) pass
```

Thus, the decision whether to provide the rich or the minimum interface hinges on selecting the design goal first.

### String's length

`string` stores its length explicitly, so that it can treat the zero-character as an ordinary character. `string_view` stores its length because this is necessary to represent a subsection of a longer character sequence, and because zero is a valid element of that sequence.

In contrast, the contract of `zstring_view` is that it will ultimately be passed to a function like POSIX [`open`](https://www.man7.org/linux/man-pages/man2/openat.2.html) and its size will be determined by iterating throughout the sequence until the zero-character. The iteration will be performed, no matter what we do! Keeping the precomputed length doesn't add value here, but on the other hand would pose a new problem: this explicit size and the result of `strlen` would have to be kept in sync, and this is difficult when zero characters are present in the sequence before the end of explicit length.

```
string s("A\0B", 3);
string_view sv("A\0B", 3);
zstring_view zs("A\0B", 3);

assert (s.length() != strlen(s.c_str()));   // ok: `strlen(s.c_str())` is not the length
assert (sv.length() != strlen(sv.data()));  // ok: `strlen(sv.data())` is not the length
assert (zs.length() != strlen(zs.c_str())); // disaster: `strlen(zs.c_str())` is the length
```

Assuming the goal of `zstring_view` is "C-string with runtime contract enforcement" or "type-tagged `const char*`", a consistent solution would be one of:

1. Do not provide member `length()` or `size()`.
2. Make these members equivalent to `strlen(c_str())`.
3. Add a precondition in every constructor, potentially runtime-enforced, that zero-characters in the middle are not allowed.

It may still make sense to have the `size_t` member but with a different interpretation. If `zstring_view` provides `operator[]`, this member could be used as an aid to runtime-enforce the precondition that the index is "in the right range".

Option #2 still requires other questions to be answered: shall this value of length be computed upon construction? If so, this a waste in the very basic use case:

```
zstring_view zs("may contain a \0 char"); // 1st range iteration
fopen(zs.c_str());                        // 2nd range iteration
```

We could compute the length when it is first needed and then cache the result. But this causes data-race issues.

### What do current implementations do?

Different implementations do different things and pursue — consciously or not — different design goals. In fact, whoever decides to implement "something like `zstring_view`" is not obliged to state or follow any design goals. Therefore, there is a limit to how much such research can help guide the design for a Standard Library component, where the design bar is higher: it should be founded on principles. But we can explore some implementations.

#### Beman Project » cstring_view (https://github.com/bemanproject/cstring_view)

The library offers a conversion from `const char*` with UB if the `char` array is not zero-terminated. Length is eagerly computed in the constructor, and can be later retrieved in 𝒪(1). Middle-zeros are allowed.

However, libraries from Beman Project are not a good fit for studying the design. They are meant to be a proof of implementability for already proposed libraries (where design goals had been stated). The [`cstring_view`](https://github.com/bemanproject/cstring_view) library is documented as implementing [[P3655R2]](https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2025/p3655r2.html), so it cannot be used to inspire its design. That would be circular.

#### {fmt} library » cstring_view

The {fmt} library ([https://github.com/fmtlib/fmt](https://github.com/fmtlib/fmt)) defines type `basic_cstring_view` (and the accompanying `cstring_view` alias) with only three non-defaulted member functions:

1. Converting constructor from `std::basic_string`.
2. Converting constructor from `const char*`.
3. Accessor `c_str`.

#### Microsoft's GSL » zstring (https://github.com/microsoft/GSL/blob/main/include/gsl/zstring)

Microsoft's GSL used to have more types dedicated to enforcing the C-string contract, but since version 4.0.0, they become obsolete ([[GSL400]](https://devblogs.microsoft.com/cppblog/gsl-4-0-0-is-available-now/)) and the only thing that is left is [`zstring`](https://github.com/microsoft/GSL/blob/main/include/gsl/zstring). It is a type alias on `char*`. No runtime enforcements, just a name marker. This is what C++ Core Guidelines ([[CPPGUIDE]](https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#gslview-views)) end up recommending using.

#### NVIDIA's implementation

We do not have access to NVIDIA's implementation of `cstring_view`, but we can gather from the description in [[P3655R4]](https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2026/p3655r4.html) that the likely goal was to enable a gradual modification of the code base. If so, it required an 𝒪(1) conversion from `cstring_view` to `string_view`. This appears close to "A C-string with additional precomputed length".

Implementations of "something like `zstring_view`" exist in quantity, but they do not necessarily agree on their primary goal, often they state no goal.

## Recommendations

LEWG should not approve any paper proposing a library component that does not clearly state its design goal. This is necessary for everyone in WG21 to be clear on what the goal is, to be able to assess if the goal is worth pursuing, and if the proposed solution actually addresses the goal.

We observe that [[P3655R4]](https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2026/p3655r4.html) does not express the goal clearly enough. Without this LEWG cannot design the type properly. It can only poll who likes which function better.

The observation that many people demand to have a type called "zstring_view" and that many people implemented their type called "zstring_view" is misleading. These implementations by different parties have different semantics and serve different goals. As we have shown, designing for one goal compromises other possible goals.

Discussions among the experts in the reflector revealed that they work with different implicit assumptions as to what the goal of `zstring_view` is. For instance, [[P4227R0]](https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2026/p4227r0.pdf) argues that zero characters in the middle shall be considered a bug and ideally prevented. This position is correct for a type whose goal is "C-string with runtime contract enforcement". The opponents argue that such prevention of zero characters in the middle is unnecessary and actively harmful. Such position is correct for a type whose goal is "`string_view` with `c_str()`".

If the goal for `std::zstring_view` is not clearly stated the worst projected outcomes may be:

1. We end up with an uncoördinated combination of design choices that in total satisfy nobody.
2. Experts caught off guard will be making decisions they do not want to, due to confusing the type's goal with what they assume to be the type's goal.
3. C++ will have its `std::zstring_view` type, but the users will still need to devise their own "string view" type that actually works.

While [[P3749R0]](https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2025/p3749r0.html) raises other objections against [[P3655R4]](https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2026/p3655r4.html) our paper focuses solely on defining the goal clearly. Once this is settled, only then can we start a due critique based on the stated goal, including the reevaluation of [[P3749R0]](https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2025/p3749r0.html).

### User confusion

However, one piece of critique can be added already, irrespective of which goal for `zstring_view` is declared and accepted.

Given that the experts incorrectly assume the goals of the type, it is reasonable to expect that so will ordinary users. They may be using a type incorrectly assuming its goal, and therefore semantics.

Given this, not having a `std::zstring_view` should be considered as a viable option.

## References

- [CPPGUIDE] — Bjarne Stroustrup, Herb Sutter, "C++ Core Guidelines", (["https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#gslview-views"](https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#gslview-views)).
- [GSL400] — Dmitry Kobets, "GSL 4.0.0 is Available Now", (["https://devblogs.microsoft.com/cppblog/gsl-4-0-0-is-available-now/"](https://devblogs.microsoft.com/cppblog/gsl-4-0-0-is-available-now/)).
- [N3442] — Jeffrey Yasskin, "string_ref: a non-owning reference to a string", (["https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2012/n3442.html"](https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2012/n3442.html)).
- [P2176R0] — Andrzej Krzemieński, "A different take on inexpressible conditions", (["https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2020/p2176r0.html"](https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2020/p2176r0.html)).
- [P3081R1] — Herb Sutter, "Core safety profiles for C++26", (["https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2025/p3081r1.pdf"](https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2025/p3081r1.pdf)).
- [P3655R2] — Peter Bindels, Hana Dusíková, Jeremy Rifkin, Marco Foco, Alexey Shevlyakov, "std::zstring_view", (["https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2025/p3655r2.html"](https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2025/p3655r2.html)).
- [P3655R4] — Peter Bindels, Hana Dusíková, Jeremy Rifkin, Marco Foco, Alexey Shevlyakov, "std::cstring_view", (["https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2026/p3655r4.html"](https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2026/p3655r4.html)).
- [P3697R0] — Konstantin Varlamov, Louis Dionne, Alisdair Meredith, "Minor additions to C++26 standard library hardening", (["https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2025/p3697r0.html"](https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2025/p3697r0.html)).
- [P3705R2] — Eddie Nolan, "A Sentinel for Null-Terminated Strings", (["https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2025/p3705r2.html"](https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2025/p3705r2.html)).
- [P3749R0] — Jan Schultke, "Slides in response to P3655R2 Concerns regarding std::zstring_view", (["https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2025/p3749r0.html"](https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2025/p3749r0.html)).
- [P4227R0] — Andreas Weis, "What's in a `cstring_view`?", (["https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2026/p4227r0.pdf"](https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2026/p4227r0.pdf)).
