I'm trying to use modern string-handling approaches (like std::string_view
or GSL's string_span
) to interact with a C API (DBus) that takes strings as null-terminated const char*
s, e.g.
DBusMessage* dbus_message_new_method_call(
const char* destination,
const char* path,
const char* iface,
const char* method
)
string_view
and string_span
don't guarantee that their contents are null-terminated - since spans are (char* start, ptrdiff_t length)
pairs, that's largely the point. But GSL also provides a zstring_view
, which is guaranteed to be null-terminated. The comments around zstring_span
suggest that it's designed exactly for working with legacy and C APIs, but I ran into several sticking points as soon as I started using it:
Representing a string literal as a
string_span
is trivial:cstring_span<> bar = "easy peasy";
but representing one as a
zstring_span
requires you to wrap the literal in a helper function:czstring_span<> foo = ensure_z("odd");
This makes declarations noisier, and it also seems odd that a literal (which is guaranteed to be null-terminated) isn't implicitly convertible to a
zstring_span
.ensure_z()
also isn'tconstexpr
, unlike constructors and conversions forstring_span
.There's a similar oddity with
std::string
, which is implicitly convertible tostring_span
, but notzstring_span
, even thoughstd::string::data()
has been guaranteed to return a null-terminated sequence since C++11. Again, you have to callensure_z()
:zstring_span<> to_zspan(std::string& s) { return ensure_z(s); }
There seems to be some const-correctness issues. The above works, but
czstring_span<> to_czspan(const std::string& s) { return ensure_z(s); }
fails to compile, with errors about being unable to convert from
span<char, ...>
tospan<const char, ...>
This is a smaller point than the others, but the member function that returns a
char*
(which you would feed to a C API like DBus) is calledassume_z()
. What's being assumed when the constructor ofzstring_span
expects a null-terminated range?
If zstring_span
is designed "to convert zero-terminated spans to legacy strings", why does its use here seem so cumbersome? Am I misusing it? Is there something I'm overlooking?
It's "cumbersome" in part because it's intended to be.
This:
Is not a safe operation. Why? Because while it is true that
s
is NUL terminated, it is entirely possible that the actuals
contains internal NUL characters. That's a legitimate thing you can do withstd::string
, butzstring_span
and whomever takes it can't handle that. They'll truncate the string.By contrast,
string_span/view
conversions are safe from this perspective. Consumers of such strings take a sized string and thus can handle embedded NULs.Because the
zstring_span
conversion is unsafe, there should be some explicit notation that something potentially unsafe is being done.ensure_z
represents that explicit notation.Another problem is that C++ has no mechanism to tell the difference between a literal string argument and any old
const char*
orconst char[]
parameter. Since a bareconst char*
may or may not be a string literal, you have to assume that it isn't and therefore use a more verbose conversion.Also, C++ string literals can contain embedded NUL characters, so the above reasoning applies.
The
const
issue seems like a code bug, and you should probably file it as such.