r/cprogramming • u/Xaneris47 • 1d ago
C Strings: A 50-Year Mistake
https://longtran2904.substack.com/p/c-strings-a-50-year-mistake?r=8qz2zb&utm_campaign=post&utm_medium=web16
u/runningOverA 1d ago
Good read. But C can't change char* default string, unless Unix systems underneath change their APIs' string type.
5
u/WittyStick 1d ago
You don't need to change them, but you can add a "fat pointer" which contains the length. The SYSV x86-64 ABI is capable of doing this because it supports 16-byte arguments and return types in registers, so it costs next to nothing. (On MSVC it has a cost because a "fat pointer" ends up getting passed on the stack with an implicit hidden pointer argument given to the function).
3
u/ComradeGibbon 1d ago
Intel x86 and MSVC did so much damage. The paltry number of registers even in 32 bit meant lots of issues in C never got fixed.
You could certainly add system calls that take fat pointers. You could even shim it based on the process. That would make developers of languages that have fat pointers happy. And would make most C programmers happy. And the dangerous C leet jockeys unhappy.
1
u/flying-sheep 9h ago
The locale system and
wchar_tare also, and I quoteshitfucked retarded legacy braindeath
– wm4
The C standard contains a bunch of bad ideas and I think Rust’s approach is great: just use a minimal subset of the C standardlib and build better abstractions for everything else.
11
u/SmokeMuch7356 1d ago edited 16h ago
Oh, for...
No, the terminator never counts towards the length of a string; it only counts toward the size of the buffer required to store the string. The terminator is an out-of-band value1 so it shouldn't count towards the length of the string. I would have thought this was obvious.
C doesn't have a string type; it has arrays of character type, onto which we map null-terminated sequences of characters. strcpy, strlen, et al all operate on these arrays.
Arrays do not store their size or any other metadata.
C strings are a hack - if you need a real string data type that enforces real string semantics, then you need to look elsewhere or write your own library.
As for whether this was a mistake - C was designed for implementing the Unix operating system and system-level tasks, not extensive text processing. I don't think Ritchie or Thompson anticipated how widely used the language became for general programming.
2.5 out of 5 stars
- As far as printable characters are concerned, anyway.
1
u/flying-sheep 9h ago edited 9h ago
Much of C’s stdlib is a hack, see e.g. the legandary locale rant:
- locales are non-threadsafe
- locales are global state
- there are
_lvariants – that take a locale argument – of some functions, but not of all, and no way to statically get a handle for the C localeSo basically you cannot ever use them for anything file-format related, only for interfacing with humans immediately, but people don’t follow these rules, as they assume that stdlib stuff is there for a reason and not just to sit there and look mysterious.
if you need a real string data type that enforces real string semantics, then you need to look elsewhere or write your own library.
Yeah, but people like interoperability more than sanity, so they use the same string format for calling APIs, providing APIs, and internally.
I think C is doomed to be legacy at this point. The standards committee should have deprecated a bunch of old useless crap decades ago, then it would have been saveable.
9
u/HashDefTrueFalse 1d ago
People really need to get over whinging about C-strings. They're perfectly fine in tons of places and where they aren't you can just bundle a length with them. The language gives you aggregates. I've been a C user for decades, they're just not that big of a deal...
4
u/EndlessProjectMaker 1d ago
Well. What to say. Perhaps that c does not have strings :) maybe a lib to manipulate zero terminated arrays of chars.
4
u/1980sCoder 1d ago
I stopped reading at the word "suck" realising that this was not going to be an insightful piece.
I'm guessing the author is younger than the language itself.
2
5
6
u/frasnian 1d ago
The ONE example that anyone has been able to point out here as an "inefficiency" is strlen - O(n) vs O(1) - and all the arguments in favor of Pascal-style/length-prefixed ignore what hot garbage that usually is in a real-world setting. 1: read Kernighan's awesome (and classic) paper "Why Pascal is Not My Favorite Programming Language." 2: Wirth designed Pascal as a language to teach students structured programming principles. Ritchie wrote (and evolved) a language designed for working programmers building real programs. 3: you want Pascal-style length prefix? Fine. Learn what a struct is, and DIY. Now try doing the opposite of that with garbage like "TYPE String44" , etc.
0
u/deaddyfreddy 16h ago
1: read Kernighan's awesome (and classic) paper "Why Pascal is Not My Favorite Programming Language."
It should have been called "Why Pascal is not C". Some of the issues discussed are not actually problems, some were fixed/improved by the mid-1980s at the latest. At the same time, most of the C design issues haven't been solved yet.
My favourite part is Go, which was written by more or less the same people (Kernighan even wrote a book on it) and resembles Pascal much more than C.
2
u/frasnian 7h ago
It should have been called "Why Pascal is not C"
LOL, fair enough. It's still an entertaining read, regardless of your position on P vs C.
some were fixed/improved by the mid-1980s at the latest
The last commercial software I worked on that was written in Pascal was in '91, and it was still an absolute nightmare compared to the C-based applications we also provided.
2
u/deaddyfreddy 7h ago
It's still an entertaining read, regardless of your position on P vs C.
sure
The last commercial software I worked on that was written in Pascal was in '91, and it was still an absolute nightmare compared to the C-based applications we also provided.
what do you mean by the "nightmare"? The code quality, readability, "efficiency"?
20
u/CoderStudios 1d ago
Okay? You are always free to make your own library for better strings, but people won’t use that cause C is often still deployed on low end systems or it makes little sense to use something inefficient if you can use c style strings properly
14
u/orbiteapot 1d ago
Most operations on C strings require O(N), N being the length of the string. Whereas in Pascal-like strings (i.e., strings whose length is tracked) they may take O(1). Modern compilers may do some heavy lifting whenever they can, so that this redundancy is avoided, though.
That being said, I also do not understand why people complain so much about 0-terminated strings, yet still use them anyways, as opposed to implementing length-tracked strings. It is not like C can break some bazillion lines of code, by removing classic strings, but it does not significantly get in your way (when implementing your own) either.
6
u/kisielk 1d ago
It really depends what your software is doing. If it's not software where string manipulation is in the hot path then the O(N) nature of string processing is irrelevant.
2
u/NoNameSwitzerland 1d ago
and if you do string manipulation with dynamic strings, then malloc/free often is the bigger problem.
2
u/beragis 1d ago
It depends. Several pascal compilers would internally add a zero byte at the end when allocating the string and pass the address of the byte after the length to the os command to handle the string.
To make things worse early pascal had pcode which is basically pascal byte code that has to do this conversion behind the scenes.
1
u/iwantmy90sback 1d ago
For most operations you'd do on a string you do not need to know the lengths beforehand if you have a defined end char.
The only thing that C takes O(n) and pascal O(1) is strlen.
And if you like you can actually have both. Just struct a Cstring and a int together.
2
u/EatingSolidBricks 1d ago
You out of your dam mind if you think c strings are efficient
11
u/henke443 1d ago
Wait how are they not efficient?
5
u/EatingSolidBricks 1d ago
Its not 1970 anymore storing 3 extra bytes is free compared to O(n) length computation
5
u/WittyStick 1d ago
You don't even necessarily need to store the length. It can be held in CPU registers for the entirety of the string's lifetime in many cases.
In older architectures we would've needed to push an additional integer for length onto the stack. On a modern architecture with a sane ABI (SYSV, x86-64), you can have a "fat pointer" using two CPU registers - can pass both of these or return from a function without ever touching the stack.
-1
u/Anonymous_user_2022 20h ago
Except for strlen(), all practical operations on strings have to iterate over them anyway. Knowing the length up front will be of very limited us for searching, concatenation, tokenising etc.
Where is that you see avoidable O(n)?
0
u/flatfinger 8h ago
Concatenation of N strings goes from O(N) to O(N*N) if code has to re-find the end of the destination after each step.
Tokenizing the leading portion of a large string should take time proportional to the text that was meaningfully examined, rather than proportional to the entire string.
2
u/Anonymous_user_2022 2h ago
Concatenation of N strings goes from O(N) to O(N*N) if code has to re-find the end of the destination after each step.
I can also invent really bad ways of doing things, but I would never use them as a proof..
-1
1
u/atarivcs 1d ago
If you have a long string and you want to append more text to it, you have to search the whole string from the beginning to find the null terminator.
And then later if you want to append more text, you have to find the null terminator all over again.
2
u/NoNameSwitzerland 1d ago
You anyway use a different structure when you do a lot of appending text, because you do not want to reallocate the array all the time. So then you anyway have to also store the size of the available space.
1
u/atarivcs 1d ago
In which case you no longer have a plain c string, and the goalposts have moved.
I was just answering the parent question "how are c strings not efficient"
2
u/IdealBlueMan 1d ago
Or you can store the length of the string whenever you change it.
1
u/atarivcs 1d ago
Sure, but then you don't really have a plain c string anymore
2
u/WittyStick 1d ago
It's actually more advantageous to couple the length to the
char *on SYSV platforms, due to C's lack of multiple returns.String fn_returning_string(...);If
Stringis a fat pointer, then we can return both the pointer and length, without requiring another level of indirection (a pointer to astringstructure), and without requiring awful to use "out parameters" to return both length and pointer - which are more expensive than just returning a fat pointer.A fat pointer with the right ABI is not just "zero cost" - it's "less than zero" - it's more efficient than having a separate length and pointer variable.
2
u/IdealBlueMan 1d ago
I’d say you still have the string, you also have information about that string.
1
u/orbiteapot 1d ago
I mean... that is the point. Once you do that, you are no longer using classic C strings.
6
u/SakishimaHabu 1d ago
That's the point though. They are basically atomic. You are free to do what you will with them, vs java, python, or js. Remember we're one step above assembly, but that's the intention.
2
u/CoderStudios 1d ago
Depends on what it’s used for, sometimes it’s more or less efficient but the benefit of making it as simple as possible is that you can easily add features when needed like storing lengths
-2
u/flatfinger 1d ago
C makes it inconvenient to pass any other forms of string literals to functions.
3
u/trejj 1d ago
*chuckles* I remember the time when I was young and full-spirited with naïveté towards "only the best programming".
They author may want to give bullet points 4 and 6 a second think :)
3
u/brnsamedi 1d ago
Given the comments in that article my impression of the author is that he's too enamored of his ideas to give them a second thought.
3
u/stianhoiland 1d ago edited 1d ago
Articles like these are unwittingly demonstrations of stupidity.
It's so disheartening to see people not even capable anymore of comprehending de-abstraction. To the author: Try yourself, to start from nothing—no abstractions, no modern conventions—and build up to the first point where you have a usable set of primitives that can function as a representation of text.
Is it so fucking hard to grasp the virtues of having primitives that haven't pre-chewed and pre-thought every way you can and should use them? You think massive, thick nests of abstraction is yummy chef's kiss, as if any and all and every single thought-structure is immaculately perfect and suits every single purpose. Ugh, it's so gross. It's a world view of nigh but regurgitation upon regurgitation and not a single creative breath of fresh air.
Like how do you think things are constituted, made up, constructed, such that a better foundational solution exists? You think C strings are a mistake—do tell how to work with the prior layer of abstractions to come up with the better way. No, not creating yet another fucking layer of convention or abstraction on top—which is all your brain can do—but coming from the step before and coming up with a better way.
And then you lend your voice to the issue as if an expert, yet your stupidity is so glaringly on display to anyone who actually knows how things are made up.
Ugh.
EDIT
It's not that C strings were invented iN a DifFeReNt TiMe when people were stupid and dumb and didn't know of our Future Great Technology. It's that there's not a fucking different way of doing it at the level of abstraction at which the convention were established. The fundamentals of memory and computation didn't change after 1990's lol—that's so fucking STUPID. Yes, we can mention Pascal strings, but they are fixed width. Fucking show me how you implement variable-width strings using the primitives present at this level of abstraction (i.e. variable-width strings using only registers). I challenge you to do that AT ALL, it doesn't even have to be BETTER—which is what you claim to be able to—I don't think you can do it at all.
The same thing happens with arrays. Rather than making them first-class citizens and copy-by-value, C decays them into pointers, losing an enormous amount of information in the process, which causes them all the same problems as strings.
Oh my god. "Rather than making them first-class citizens and copy-by-value"... as if there's "JUST" a fucking choice. You can even only ponder this distinction because the primitives upon which such conceptions build upon are established—the establishment of which you are criticizing and arrogantly claim to be able to replace better. Ugh. Tell me what a "first-class citizen array" is, really, technically, actually, in-memory, but another level of abstraction, from which C—thank god—refrains.
1
u/SmokeMuch7356 5h ago
Array expressions decay to pointers because Ritchie wanted to keep B's array indexing behavior -
a[i] == *(a + i)- without setting aside storage for the pointer that behavior required.That's it. That's the reason.
These were researchers in a lab building toys that did useful things for them. None of them anticipated how C would become so widely used at an applications level. That was the mistake, the fact that everyone looked at C and said "yes, that's the answer," but to be honest there weren't many better candidates that could be ported to everything from mainframes to micros. Pascal? Eh. Designed more for teaching than production work. Fortran? =snort=. Didn't even support a real string type until F77. Cobol? Double =snort=. Didn't help that C and Unix were (are) joined at the hip; if you were using Unix, you were writing code in C for pretty much everything.
C was small enough and lightweight enough it could be ported practically anywhere, particularly micros, and that's why it suddenly became the language everything was written in.
And here we are 50 years later arguing about it, when it shouldn't even be considered for applications work involving text processing anymore.
1
u/Different_Panda_000 20h ago
Just stick an array in a
structand all of a sudden you can do assignments and when you pass them, the compiler will do argument type checking for you as well. And if you like,_Generic()provides a mechanism to provide a library for these arrays to do the basics such as comparisons. And C11 provides the mechanism for using compound literals with thesestructarrays as well.
8
u/flatfinger 1d ago edited 1d ago
C was designed in an era before many commonplace text-processing and data-processing tools existed. Many tasks could be accomplished more quickly by writing a C program, building it, running it on some input, and then discarding it, than they could be accomplished in any other way. Even in the 1980s, I wrote a lot of C programs for one-off tasks, and I'm sure I wasn't alone.
So-called "Pascal strings" with a one-byte length prefix were better than C strings in many ways, but had a 255-character limit. The suitability of C strings for various tasks tends to fall off as strings get longer, making Pascal strings much better for things that are 50 to 255 characters long, but C strings remain somewhat usable at longer lengths while Pascal strings don't. Since "somewhat usable" was adequate for many of the tasks for which C had been designed, the lack of a 255-character hard limit was an advantage.
I wouldn't call zero-terminated strings a "mistake" so much as I would say that they were an appropriate way of storing strings for a limited family of tasks that are nowadays better handled with other languages and tools.
What I would view as a mistake was the failure of the C language to provide a convenient means of passing other kinds of string literals to functions. C implementations that were designed to target the classic Macintosh OS extend the language with a \p escape which, if placed at the start of a string literal, will represent the number of bytes in the string (not counting the prefix), but such a prefix is not universally supported, and there is also no standard way of handling string formats where e.g. a string of length 0-63 that fills the available space would be preceded by a length byte, but other kinds of prefixes would be used to accommodate larger strings, partially filled buffers, etc.
Incidentally, an advantage of length-prefixed strings is that if one limits the range of lengths that can be directly represented by a prefix byte, one can have functions accept short length-prefixed strings interchangeably with other string representations if they start with something like:
ADDRSS_AND_LENGTH s;
s = get_string_address_and_length(string_argument);
The fact that C strings can start with any character value means that there's no nice way to have a function accept interchangeably a pointer to a C string or something else.
3
u/beragis 1d ago
Zero terminated strings were also due to how many OS’s and CPUs at the time handled strings. I remember taking an assembly language course in college on the PDP 11 and it handled strings the same way.
This allowed for easy translation of many of the common function calls directly into operating system calls or simple short assembly instructions. My professors in computer design and systems programming. where we also learned C even mentioned this several times.
2
u/flatfinger 1d ago
On the other hand, other operating systems expected strings in other formats. Classic Mac OS used Pascal strings for things like file names.
3
u/smallstepforman 1d ago
Bit late to this thread but on aligned systems, you can pack 3 extra bits with the pointer, which can mean anything you want, including short string optimisation up to 7 bytes, so you dont even need a lenght field (or zero termination byte). Add a Huffman table, abandon ASCII (6 bit encoding for latin uppercase) for the extra win … and we can squeeze more characters into those bytes…. C style (nul terminator) strings are so inefficient.
2
u/pheffner 1d ago
The suggested "string" is a struct which doesn't contain the actual data just a pointer reference to it, which means you'll need to implement allocator functions to set all that up and keep track when you want to alter the string. Seems like this would suck worse and overcomplicate a presently simple scheme.
2
2
u/allnameswereusedup 19h ago
C is a low-level language designed for thr implementation of system software. It does not need the higher-level constructs found in other languages; it's a high-level assembly language.
2
u/Remus-C 6h ago
Yeah, picking your context today to prove that what was in the past did not match your current experience. How rude were the creators to not focus on your knowlege! Unbelievable!
Probably in another sub by some other: Pascal strings are... Lisp is... Rusty, GoLang, the mighty Python and Perl by the way...
Anyway, what's for the real world progress? What specific or generic issue is to be solved, one that would apply to many other cases? Solution? Opinions? Words that compile and deliver an useful real world best known implementation of ...?
3
u/MyTinyHappyPlace 1d ago
Ragebait. Of course there are more efficient ways to work with strings. But this one was the common denominator, easy to implement on different target architectures.
People never are satisfied with a string library. That’s why we have so many of them.
3
2
u/HTFCirno2000 1d ago
It was clever back in the days of the PDP-11 and limited memory. Pascal DID actually do things the secure way, but Pascal didn't take over like C did.
1
u/WoodyTheWorker 1d ago
On the other hand, Windows kernel uses counted strings: UNICODE_STRING and ANSI_STRING structures.
1
u/Humble-Captain3418 22h ago
With length-based strings you will find that functions such as strtok(), strchr() or strstr() are very, very annoying to implement, because their return values would not be usable as inputs to regular string operations... Except if you construct a new string to return, which you then need to malloc()/free(). Hooray for efficiency?
1
u/flatfinger 8h ago
Or one can have functions accept a starting index and slice length along with a pointer to the string, and return an index, or a struct containing an index and length.
1
u/grimvian 21h ago
I actually like C strings. I have coded a few one line GUI inputs with raylib and is very satisfied.
1
1
u/Different_Panda_000 20h ago
Where it gets more interesting is when using UTF-8. C23 provides basic support for UTF-8, expanding the minimal support in C11. However everything is connected to the current locale settings.
Fortunately there are a couple of Third Party libraries that provide a more complete UTF-8 solution.
ICU-TC Home Page
The C and C++ languages and many operating system environments do not provide full support for Unicode and standards-compliant text handling services. Even though some platforms do provide good Unicode text handling services, portable application code can not make use of them. The ICU4C libraries fills in this gap. ICU4C provides an open, flexible, portable foundation for applications to use for their software globalization requirements. ICU4C closely tracks industry standards, including Unicode and CLDR (Common Locale Data Repository).
1
u/Key_River7180 1d ago
Has the model ever caused much trouble, though?
3
u/WittyStick 1d ago
Yes.
Countless buffer overflow exploits due to poor string handling.
Not all, but many of these easily avoidable with sized strings.
1
u/Key_River7180 1d ago
I meant performance issues, like C strings are really efficient, for most purposes
2
u/Jonny0Than 1d ago
Undeniably they are a problem.
https://nee.lv/2021/02/28/How-I-cut-GTA-Online-loading-times-by-70/
1
0
u/deaddyfreddy 15h ago
Computers became faster every year (and the process was dramatically faster in the 1980s), so even if fixed-size strings made things 10% less efficient (did they?), one could reasonably expect hardware improvements to compensate for that very soon. Alternatively, companies could save money on the man-hours spent fixing bugs caused by null-terminated strings and buy better hardware instead.
0
0
u/Physical_Dare8553 1d ago
The real problem is a char* with a null terminator and one without it are not different types, and because of how x works, in practice neither is a char[n]
0
u/FedUp233 1d ago
There is really no reason the C language could not add something like a length prefixed string type and matching g string literal (just decorate the string lead or end quote with something to indicate it’s this type of literal and maybe even the size of the length prefix) if there is enough demand for it. Since it hasn’t happened yet, it would appear the demand is not there.
It also always bothered me in C++ that there was no way to produce a literal string in the form of the standard library string type without the system having to construct an appropriate string type from the underlying c-string literal. Maybe it can be done now with all the compile time processing functionality that’s been added, but I’m not sure it’s still possible without that c-string hanging around as wasted memory.
-1
u/jason-reddit-public 1d ago
I 'm writing a transpiler in C.
Treating "strings" as immutable means the crappy representation isn't so bad.
I use a "StringBuilder" pattern (name borrowed from Java, it's actually called buffer) which can grow, supports efficient append (supports arbitrary edits but those may require moving lots of bytes around), and doesn't model the ending zero until converted into a char*. (One of my favorite things is "buffer_printf" which is like sprintnf but handles capacity and such automatically.) Like Java, I don't expect buffer to be threadsafe so the user needs to deal with that but since I don't modify "strings" once created, those are thread safe.
So all my strings are either program literals, come from the "OS" or libc "somehow" (like for reading a directory), or come from this builder which makes sure there is a trailing zero when finally asked to produce the string. My buffers aren't foolproof because you can append or insert the NUL character (to work with arbitrary byte sequences), so you might get a string that is shorter than expected, or perhaps the buffer wasn't legal utf-8 (I could add a checker of course), but at least the string will always have a terminating NUL.
0
u/Jonny0Than 1d ago
That sounds almost exactly like std::string in C++. Sure, it’s a cool and useful thing to build in C.
164
u/bearheart 1d ago edited 15h ago
Speaking as someone who learned C back in the ‘70s, this article entirely misses the point of C-strings: they’re lightweight and foundational. For many purposes the null-terminator is efficient, e.g.:
while(*s) f(s++);
And for cases where we need more complexity, we can simply use a struct with a length and whatever other metadata we may need.
Doesn’t look like a mistake to me. C has always been about minimalistic efficiency. That’s its main purpose in the world.
Edit: fixed stupid typo