Date: Sun, 1 Feb 2026 11:16:27 -0500
On Sun, Feb 1, 2026 at 1:03 AM Thiago Macieira via Std-Proposals <
std-proposals_at_[hidden]> 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
<https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2025/p3655r3.html>, 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
std-proposals_at_[hidden]> 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
<https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2025/p3655r3.html>, 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
Received on 2026-02-01 16:16:45
