On Sun, Feb 1, 2026 at 1:03 AM Thiago Macieira via Std-Proposals <std-proposals@lists.isocpp.org> wrote:
On Saturday, 31 January 2026 21:42:33 Pacific Standard Time Jan Schultke wrote:
> Opinions on this are obviously a bit torn, but LEWG essentially decided to
> have this already. In Sofia, there was no consensus to ban nested nulls,
> and the status quo is to allow them. P3655R3 also has this constructor.

Indeed. I think it's a mistake.

> The point of this constructor is to permit zero-overhead construction from
> an existing null-terminated C-string, with size information available. This
> case is extremely common when initializing from strings returned by C APIs
> or from user-defined containers in the style of std::string.

If you ban embedded NULs (or, phrased better, pretend they don't exist), then
conversion from basic_string is fine.

I don't buy the argument on conversion from existing C strings. Those usually
don't have a length passed in the first place. And where an strlen() has been
done, as I said in my reply, compilers are good at remembering the value.

I agree that C APIs that return null-terminated strings don't also return the length. (Where could they?)
However, it's not obvious to me that "conversion from basic_string is" ever what I'd call "fine." Basically we have the following two ways to get a "{c,}string_view" out of a std::string:

    auto s = std::string("hello\0world", 11);
    std::string_view sv1 = s;  // sv1.size() is 11
    std::string_view sv2 = s.c_str();  // sv2.size() is 5
    std::cstring_view cv1 = s;  // cv1.size() is 11, right?
    std::cstring_view cv2 = s.c_str();  // cv2.size() is definitely 5
    
Now, looking at P3655R3, I do find it weird at first how the constructor used by `cv1` is specified:
template<typename T>
  concept cstring_like = requires(const T & t) { { t.c_str() } -> std::same_as<const T::value_type*> };
template<class R>
  constexpr basic_cstring_view(cstring_like R&& r);
Constraints: [...]
— R models ranges::contiguous_range, ranges::sized_range and cstring_like
[...]
Postconditions: size_ returns ranges::size(r) and data_ returns ranges::data(r).

That is, this constructor doesn't participate unless `r` has a .c_str() method — and yet, it never calls that .c_str() method. Instead it calls `data(), size()`.
This is what creates the weirdness above, that cv1 and cv2 end up with different contents.
However, since sv1 and sv2 also end up with different contents, I think this is actually a reasonable API. Here our `.c_str()` method isn't acting as a customization point; it's acting merely as an opt-in tag: types that declare `.c_str()` are assumed to have the all-important null terminator, and types without `.c_str()` are assumed not to have one.

The only thing I'd like to see changed about the current P3655R3 API is where it says:
    template<typename T>
      concept cstring_like = requires(const T & t) { { t.c_str() } -> std::same_as<const T::value_type*> };
This is the only place right now where a `value_type` member typedef is required, AFAICT. I would rather see this use `ranges::range_value_t<T>` — in fact `s/typename T/class R/` — or, probably even better, make `charT` a parameter of the concept so that we can just say "R models ranges::contiguous_range, ranges::sized_range, and cstring_like<charT>."

I wish there were a sane way to express the precondition that r.data() must return the same value as r.c_str(), but I don't think there is.

–Arthur