The zero-terminated string is by far C's worst design decision. It is single-handedly the cause for most performance, correctness, and security bugs, including many high-profile CVEs. I really do wish Pascal strings had caught on earlier and platform/kernel APIs used it, instead of an unqualified pointer-to-char that then hides an O(n) string traversal (by the platform) to find the null byte.
There are then questions about the length prefix, with a simple solution: make this a platform-specific detail and use the machine word. 16-bit platforms get strings of length ~2^16, 32 b platforms get 2^32 (which is a 4 GB-long string, which is more than 1000× as long as the entire Lord of the Rings trilogy), 64 b platforms get 2^64 (which is ~10^19).
Edit: I think a lot of commenters are focusing on the 'Pascalness' of Pascal strings, which I was using as an umbrella terminology for length-prefixed strings.
It's usually the case that the more strident someone is in a blog post decrying innovation, the more wrong he is. The current article is no exception.
It's possible to define your own string_view workalike that has a c_str() and binds to whatever is stringlike can has a c_str. It's a few hundred lines of code. You don't have to live with the double indirection.
System APIs requiring passing a null-terminated string are also painful to use from other languages, where strings are not null-terminated by default. They basically require taking a copy of a string and adding a null-terminator before performing a call.
I work on embedded computers with mostly around 64K RAM using C99. Any form of alloc is forbidden. So I implemented a string lib that works with what is called here as views. I hold length and contend in preallocated arrays. Each string has exactly 127 characters and is also zero-terminated to fulfill C-API needs, and my tables can hold between 16 and 64 strings depending on the project. There is even a safety zero at index 127 enforced in any operation. This system allows for fast, non-copy workflow, and ownership is always obvious; a string is not owned. I even have different "arenas" for different parts of the system that can clear independently. I use this approach also in a desktop context, albeit scaled up in length and number. This combines view, zero delimiter, ownership, and arena-like management altogether.
Man, I really don't miss working in C++. Used to be my daily driver until I ended up in C# land. I understand why C++ is the way it is, I understand why it's still around and the purposes it serves, but in terms of the experience of using the language... I wouldn't want to go back.
It's best to avoid using std::wstring and other wchar_t-related facilities, as they are highly non-portable across different platforms. If you need to interact with the Win32 API, use char16_t and std::u16string, so that anyone knows it contains a UTF-16 encoded string and knows how to use and process it.
Starting by defining the non-owning slice references, rather than the owning container types is such a massive advantage that it's very telling that Stroustrup didn't get this right in his language.
Today Bjarne will insist that the correct understanding of ownership and lifetimes was always inherent in his language and if you point out that it basically only warrants a brief mention in his early books about the language he'll say that the newer books have much more about this, as if that's not a confession...
Because &str (the non-owning reference to a slice of UTF-8 encoded text) in Rust is so ubiquitous, it's completely reasonable in Rust to use anything from the simple standard library owning String type, which is literally a growable array, Vec<u8> plus the promise about UTF-8 encoding, through to text which lives only on the stack as a re-interpreted array, or at the other extreme a raw pointer-sized short-string optimisation https://crates.io/crates/cold-string -- where unless it fits inline the string is length-prefixed on the heap like Pascal. All these approaches fit different niches and since a mere reference to the string is the same for all of them they're compatible. In C++ you will run into too many problems and regret trying.
Edited: Correct that String lives in the stdlib -- it isn't baked in to the core language because your tiny embedded system may not have an allocator to go around creating fancy growable arrays, but it still might want the non-owning string slice, &str which truly is in the core language.
8 comments
[ 18.9 ms ] story [ 52.0 ms ] threadThere are then questions about the length prefix, with a simple solution: make this a platform-specific detail and use the machine word. 16-bit platforms get strings of length ~2^16, 32 b platforms get 2^32 (which is a 4 GB-long string, which is more than 1000× as long as the entire Lord of the Rings trilogy), 64 b platforms get 2^64 (which is ~10^19).
Edit: I think a lot of commenters are focusing on the 'Pascalness' of Pascal strings, which I was using as an umbrella terminology for length-prefixed strings.
It's possible to define your own string_view workalike that has a c_str() and binds to whatever is stringlike can has a c_str. It's a few hundred lines of code. You don't have to live with the double indirection.
Today Bjarne will insist that the correct understanding of ownership and lifetimes was always inherent in his language and if you point out that it basically only warrants a brief mention in his early books about the language he'll say that the newer books have much more about this, as if that's not a confession...
Because &str (the non-owning reference to a slice of UTF-8 encoded text) in Rust is so ubiquitous, it's completely reasonable in Rust to use anything from the simple standard library owning String type, which is literally a growable array, Vec<u8> plus the promise about UTF-8 encoding, through to text which lives only on the stack as a re-interpreted array, or at the other extreme a raw pointer-sized short-string optimisation https://crates.io/crates/cold-string -- where unless it fits inline the string is length-prefixed on the heap like Pascal. All these approaches fit different niches and since a mere reference to the string is the same for all of them they're compatible. In C++ you will run into too many problems and regret trying.
Edited: Correct that String lives in the stdlib -- it isn't baked in to the core language because your tiny embedded system may not have an allocator to go around creating fancy growable arrays, but it still might want the non-owning string slice, &str which truly is in the core language.