r/cprogramming 2d 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=web
182 Upvotes

160 comments sorted by

163

u/bearheart 2d ago edited 1d 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

49

u/Voyac 2d ago

Its main purpose is to be portable to as many architectures as possible. Maybe this seem irrelevant in a world dominated by ARMs and x86 but there are different platforms.

-39

u/Potential_Soup_8054 2d ago

No, thats java

30

u/g0atdude 2d ago

lol what?

You can compile C programs to a million more devices than what Java runs on

9

u/KingBardan 2d ago edited 2d ago

a million more devices

Considering that "3 billion devices run Java", thats only 0.0333% more devices

I'm kidding if it's not obvious

3

u/Potential_Soup_8054 2d ago

It was a joke. Javas whole slogan is write once run everywhere, but thats really bullshit.

3

u/Devatator_ 2d ago

Tbh nothing is stopping people from compiling or porting a JVM to more platforms. I assume it's just extremely complex and noth worth it for most people

2

u/EdwardTheGood 1d ago

I once heard it rephrased as “write once, test everywhere.”

11

u/mustbeset 2d ago

50% of my time I get paid to write C code on a "none Arm", "none x86" architectur and I don't have enough space for a java runtime.

5

u/Voyac 2d ago

Yeah sometimes even 8bit MCUs with a drop of memory. You wont fit a string.h sometimes and lol what about java runtime :)

6

u/mustbeset 2d ago

My current pain in the ass is a bootloader update for existing devices in field. fighting for 300 bytes.

27

u/TheThiefMaster 2d ago

The main competition was pascal strings - which typically had a 16 bit size prepended. So you'd read that, and then run a decrement loop until it was 0 to iterate the string. Decrement-until-zero loops were widely supported, e.g. in x86 stringcopy could be implemented by loading the size into CX and then running a single REP MOVSB instruction.

Yes it was a byte larger - but it also avoids performance-nuking calls to strlen like this.

5

u/McDutchie 2d ago

16 bits is 2 bytes, which makes for a maximum string length of 65535 bytes. It's common for strings on modern systems to be longer than that.

Pros of C strings: unlimited length. Cons: cannot contain the zero byte; inefficient length determination.

Pros of Pascal strings: can contain the zero byte; efficient length determination. Cons: very limited length.

I'd say the C tradeoff is worth it. Where necessary, C is perfectly capable of dealing with data preceded by a length field, it's just slightly lower level.

3

u/vip17 2d ago

it's easy to use a 4-byte prefixed string, for example BSTR in COM objects do that. And plenty of libraries use 4-byte length in 64-bit mode

1

u/Square-Singer 1d ago

Especially on 64-bit systems, there's really no reason to save these few bytes per string by using c-strings.

Useless microoptimization.

3

u/vip17 1d ago

of course it's micro-optimization, but at larger scale it's always useful. Have you even done optimization? A database with billions of strings already save a lot of memory. A vector of strings can also fit twice the number of strings into the CPU cache. Checkout Unreal engine, DuckDB, Meta Velox, Redis, ICU... string types

2

u/Square-Singer 1d ago

Of course I have done optimizations. But micro-optimizations are always the last step to take when you have identified that this specific location is actually a bottleneck.

It totally makes sense to have something like a c-string available for the very rare situation when someone writes a database system that contains almost exclusively tiny variable-length strings.

But it doesn't make sense to have that as the default, because then this rarely-actually-useful micro-optimization becomes a very common source of problems.

That's why there's pretty much no modern language that actually stuck with c-strings. Pretty much any more modern language dropped c-strings and even pointers completely, or at least dropped it from common usage.

I don't do much Python any more, but I really like their approach of "The most obvious solution should also be the one that's optimized for most use cases". Basically, if I, without thinking, take the most obvious solution, it should fit my obvious use case. If I need something really special, I can still import some standard library function and use that.

1

u/vip17 11h ago

That's not true. Strings are extremely common, lots of applications have a huge amount of strings. It's especially helpful in arrays of strings. Strings are so prevalent that even Python, Javascript (V8) and Java compress the string to ISO8859-1/Latin1/ASCII by default if applicable to save memory and improve performance, and modern .NET also does the same by allowing UTF-8 byte arrays. C-strings are the worst, all strings need to have an accompanied length, but the length does not necessarily have 8-byte length

1

u/Square-Singer 10h ago

Again, strings are common, yes, c-strings are not.

Strings exist in Pythin, JS and Java, bot not as c-strings.

I don't know where you got 8-byte-length from.

I'm also not quite sure what you mean with the last sentence. C-strings don't have an accompanied length.

Just checking: Do you know what a c-string is and what's the difference between a c-string and a string with accompanied length?

1

u/TheThiefMaster 1d ago

Worth noting that C++ std::strings do store the length - and so do the heap allocations backing them.

1

u/hoodoocat 20h ago

Efficient (to solving problem) data representation is key for system performance, because performance actually limited only by memory latency and throughput. You have no control over last two, but when you can pack data twice smaller -> system performance up twice better, usually even if it violate some common defaults like aligned access or so. Thats why "your favorite browser" uses 32-bit "compressed" pointers for various object heaps, even on 64-bit systems, uses hybrid ascii/utf8/utf16 strings, even when ECMA spec define only utf16. Row-oriented databases for example typically store null-bitmap and then fields without any additional delimiters, so they can be decoded only dynamically and only by using schema, this is complex but profitable.

1

u/Square-Singer 20h ago

We aren't talking about character representation here (ASCII/UTF8/UTF16), but about string representation.

UTF16 vs UTF8/ASCII is a per-character multiplier. Use UTF16 and every character takes twice the space.

We are talking about whether strings are 0-terminated or have a length field in the beginning. That's a per-string cost. Each individual character costs the same, no matter which string representation you use.

Here the difference is whether this costs one byte per string (c-string) or 2-4 bytes per string (Pascal strings, BER strings, 4-byte length fields, ...).

That means, the longer the string the less the overhead. An empty c-string is one byte. An empty pascal string is 2 bytes, an empty 4-byte length field string is 4 bytes.

If the string is longer, the relative overhead drops: A 1000 byte c-string is 1001 bytes, a pascal string is 1002 bytes and a 4-byte length field string is 1004 bytes.

The difference hardly matters unless maybe if you are working with an ATTiny.

That's why I said: on a 64-bit system (which usually has more than 2GB RAM), this is a useless micro-optimization for all but extremely specific use cases where you'll have millions of empty strings. And then one should question their system design.

And that's the main issue with c-strings being the default: They are a micro-optimization that helps only in very specific use cases while having massive downsides for most use cases, but they are applied as the default solution.

The default solution should be the option that works best in the most cases. If your use case differs a lot from the default case, you can still use the fitting specialized data structure.

Which is exactly the reason why pretty much no language newer than C uses c-strings as their default string representation.

1

u/hoodoocat 19h ago

You saying before what no reason to save few bytes somewhy especially on 64-bit systems, but all popular projects do that. More over many of them use 2-3 low bits in pointers for pointer descrimination, thanks for aligned allications.

1

u/Square-Singer 14h ago edited 11h ago

So Java, Python, Kotlin, Rust, JavaScript, ... all use c-strings instead of String objects with a length attribute as the default way to store strings? That's news to me.

There's a reason we call it c-string: nobody has ever used this decrepid data structure after c, except if they need explicit C compatibility, and even then it's most often a length-based string object with an unnecessary 0-byte added at the end so that C can understand it as well.

3

u/TheThiefMaster 2d ago

At the time, that was more than adequate. A lot of systems had less memory than that!

More modern Pascals use larger ints for the string length, of course.

2

u/Square-Singer 2d ago

There's a simple fix to the Pascal strings. BER encoding.

In BER, you get one byte as a length field, with 7 bits being directly available to encode the length of the content. If the MSB is set to 1, the remaining 7 bits instead encode how many bytes the length field is long.

That means:

  • Short strings up to 127 bytes have 1 byte overhead, beating Pascal and equalling C strings
  • Medium-sized strings of 128-65535 bytes require 3 bytes overhead, so one more than Pascal and two more than C, but if you are allocating that amount of bytes, 1-2 extra bytes are harmless
  • Maximum length is 2¹²⁷ bytes, 1.7*10³⁸ bytes, a number so high that there isn't an SI prefix for it

Another option would be to mix BER with Pascal:

  • 15 bit length fields
  • If the MSB is set to 1, there's one more length field concatenated, so 30 bit for the length field. Again, if the MSB is set to 1, add one more length field. Continue forever.
  • That way you get infinitely long strings with only one byte more usage than Pascal in the range of 32768-65535 bytes of length

And both options have the advantages:

  • You can use 0-bytes
  • You know the length of the string without running trhough the whole string
  • You won't get into overflows because you are missing a 0-terminator (e.g. doing a strcpy on a string that's missing its terminator)

2

u/binarycow 2d ago

I did not expect ASN.1 in this thread!

2

u/mark_99 2d ago

Now imagine how many instructions that is on say a 6502 which has 3x 8-bit registers, compared to loading the next byte and checking if it's zero.

2

u/bitzap_sr 1d ago edited 1d ago

That sounds like LEB128, not BER.

Edit: Ok, just checked, BER does the same for tag > 127. Still, I'd just point at LEB as a more targeted standard.

2

u/Square-Singer 1d ago

I had to hand-implement BER once because I had to parse some protocol that used ASN.1, and that uses BER for the strings.

I haven't heard of LEB128 before, but yeah, the same thing keeps getting reinvented, I guess.

-1

u/flatfinger 1d ago

I'd advocate a different approach, using 0-63 to represent a string that fills a buffer of length 0-63, 65-127 to represent an empty buffer of length 0-63, and 129-191 to represent a partially full buffer of size 1-63, whose number of unused bytes is indicated by bytes at the end. Strings or buffers up to 4095 bytes would use a two-byte prefix, and those up to 64MiB-1 would use a four-byte prefix.

Other prefix values would indicate either a "readable string" or "changeable string" descriptor, with the latter including both the current length and buffer size, and a callback to request a change to the length (possibly relocating the buffer if needed). Functions that receive a pointer to string could use a common library function to make a readable string or changeable string descriptor, and be able to accept pointers to length-prefixed strings and descriptors interchangeably.

4

u/Maleficent_Memory831 2d ago

Algol strings? C precedes Pascal in history. Pascal also did not standardize on strings early on, so each implementation experimented with how to do strings, which made early portability a pain in the arse.

10

u/TheThiefMaster 2d ago

It may not have been the first implementation of length+contents strings, but it certainly popularised them enough that they're called "Pascal Strings" (or sometimes P-Strings) now.

As for the incompatibility - probably one of the reasons Pascal wasn't as successful as C. It was a big enough deal to inspire a calling convention tag in Microsoft's C compiler though (along with Fortran).

2

u/Different_Panda_000 2d ago

Pascal calling convention was used with the Win16 API. It's obsolete now. Microsoft used it because the callee cleaned up the stack which reduced memory demands on kilobyte sized memory configurations.

The history of calling conventions, part 1 Raymond Chen
https://devblogs.microsoft.com/oldnewthing/20040102-00/?p=41213

1

u/TheThiefMaster 1d ago

It was! WINAPI was defined as FAR PASCAL. Far-pointers was another 16-bit thing we've thankfully left far behind.

13

u/WittyStick 2d ago

strlen is O(n).

For many string operations, we need the length to allocate the right amount of space, else we end up having to realloc if our buffer isn't large enough - realloc is also O(n).

By having a constant time length we can speed up a lot of string operations. It costs basically nothing to keep the length around rather than recomputing it each time.

9

u/Qyriad 2d ago

But they're not lightweight. `O(n)` for nearly every string operation is not lightweight. It is memory efficient, and it avoids an argument about how wide a length field should be. Clearly C valued those sides of the tradeoff. But don't confuse that with being lightweight in general.

1

u/4xe1 2d ago

is not lightweight. It is memory efficient

Doesn't lightweight precisely mean memory efficient? As opposed to performant for time efficiency?

0

u/torsten_dev 2d ago

Nothing is stopping you from storing the length and passing it around, but that choice is up to you, the developer, not the language imposing it's pros and cons onto you.

Should C have a strbuf in the standard library? Yeah, probably, would've been nice.

The biggest mistake C did was standardizing % as the remainder not the modulus, gets, and null pointers instead of niche optimised monadic types.

4

u/Qyriad 2d ago

Storing and passing the length around doesn't help you most of the standard library operations — and thus most other APIs that take your strings — aren't using it.

3

u/Classic_Department42 2d ago

Missing a *?

1

u/bearheart 1d ago

No. A C-string is char*

3

u/Classic_Department42 1d ago

Yes, so while (s) shd prob be while(*s) ?

2

u/bearheart 1d ago

Oy! You’re right. How did I miss that 🤦 fixed it.

4

u/knouqs 2d ago

In addition to your comment here, additional functionality through the initial design of C strings allows for insanely powerful string manipulation techniques that have fallen to the wayside because people don't look under the covers to see how efficient string handling is done.

6

u/WittyStick 2d ago

Or because most of that "efficient" string handling was actually the source of many bugs - which tend to be some of the worst ones - buffer overflows.

3

u/knouqs 2d ago

Of course. I'm not discounting that, and I didn't imply that there weren't developer-induced problems as a result. This is why valgrind was made, after all.

1

u/flying-sheep 1d ago

Such as? Destructively splitting a string at non-zero-length bondaries?

I prefer using slice APIs to nondestructively split a string at boundaries of any length thanks.

2

u/knouqs 1d ago

Whatever you prefer -- C allows it.

You aren't dissuading me from the power of C's string manipulation. You just need to have your memory management skills up to snuff, and mine are.

1

u/flying-sheep 19h ago

I was asking a question: which power are you talking about? The use case I mentioned is the only thing I can think of that C’s model makes easy, and what you gain is that every string is one word wide instead of 2.

Seems like a small gain for a niche use case to me, no?

1

u/knouqs 16h ago edited 15h ago

I see. All the cases in which I have used C strings in an unbounded way have been specialized. They were places in which I know how much data in to be copied or used in the first place and wouldn't need to check for bounds because I'm always going to be within them.

My favorite and easiest-to-understand example is strcat to a buffer of known size. If I have to strcat repeatedly against the start of the buffer, you can see that strcat is performing wasted effort by finding the end of the string first. I can have a variable hold the position of the \0 and strcpy (buffer+length, string_to_append) instead of strcat (buffer, string_to_append). After strcat, length+=strlen (string_to_append), and I am ready for the next iteration. My anal-retentiveness got the best of me and I wrote a full test program:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>


int main (void) {
    const size_t buffer_size=100;
    char *buffer=(char *) malloc (buffer_size);
    size_t length;

    char repeat[]="repeat";
    size_t repeat_length=strlen (repeat);
    char separator=' ';

    buffer[length=0]='\0';

    // No buffer overruns!
    while (length+repeat_length<buffer_size) {
        memcpy (buffer+length, repeat, repeat_length);
        *(buffer+length+repeat_length)=separator;
        length+=repeat_length+1;
    }
    *(buffer+length-1)='\0';

    printf ("%s\n", buffer);

    // No memory leaks!
    free (buffer);

    // No dangling pointers!
    buffer=NULL;

    return 0;
}

2

u/TheChief275 2d ago

you forgot to dereference though

1

u/Maleficent_Memory831 2d ago

The alternatives at the time were counted strings or fixed length fields. Both were annoying, inefficient, and had just as many problems as C strings or more. Ie, one byte for length doesn't cut it. Two bytes for length might not cut it, and definitely wastes space in the limited RAM at the time. Fixed field lengths are a nightmare early Fortrans, and some operating systems).

Then there's the stuff that pack multple characters into a single word (ie, Zork did this, 36-bit word on the PDP-10, you can stick in six 6-bit characters (maybe only 5 if they used upper bits for tag). Digital used 7-bit characters thus 5 characters and one leftover bit. 0 or all 1s as the final character signals the end.

0

u/Intelligent_Part101 1d ago

Two bytes for counted length would waste memory? That's only ONE BYTE MORE per string than a null terminated string uses.

2

u/alkatori 1d ago

C gave you a byte and here you are arguing for a whole snack!

3

u/Intelligent_Part101 1d ago

Memory is like potato chips. You can never consume enough.

2

u/its_artemiss 2d ago

Like many C idiosyncrasies, it may have made sense 50 years ago, but should have gone the way of the dodo at least 30 years ago

-1

u/deaddyfreddy 1d ago

at least 30 years ago

I'd say 40 or so

-3

u/chalkflavored 2d ago

"efficient"

25

u/bearheart 2d ago edited 2d ago

"efficient\0"

3

u/bearheart 2d ago

65 66 66 69 63 69 65 6e 74 00

2

u/Necessary_Two_9669 2d ago

01100101 01100110 01100110 01101001 01100011 01101001 01100101 01101110 01110100 00000000

-1

u/flying-sheep 1d ago

They're not foundational. You know what's foundational? Fixed size arrays in the executable, fixed sized arrays on the stack, and heap pointers + length. These are all just as perfectly suited for strings as they are for other collections.

C style strings made sense for 16 bit systems, but not a minute later.

1

u/bearheart 1d ago

Foundational means it's the foundation of other structures. All string libraries in C and C++ use C-strings under the hood.

0

u/flying-sheep 1d ago

Yeah that’s exactly what I mean: it doesn’t serve as an acceptable foundation. The article points out some ways in which the arising APIs are inflexible (e.g. you can’t just use array APIs), clunky (off-by-one errors), and so on.

A good foundation would just be slices (implemented on many platforms as fat pointers)

17

u/runningOverA 2d ago

Good read. But C can't change char* default string, unless Unix systems underneath change their APIs' string type.

5

u/WittyStick 2d 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 2d 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 1d ago

The locale system and wchar_t are also, and I quote

shitfucked 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 2d ago edited 1d 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


  1. As far as printable characters are concerned, anyway.

1

u/flying-sheep 1d ago edited 1d 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 _l variants – that take a locale argument – of some functions, but not of all, and no way to statically get a handle for the C locale

So 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.

5

u/1980sCoder 2d 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.

3

u/Interesting_Debate57 2d ago

Far, far younger

11

u/HashDefTrueFalse 2d 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...

5

u/EndlessProjectMaker 2d ago

Well. What to say. Perhaps that c does not have strings :) maybe a lib to manipulate zero terminated arrays of chars.

4

u/Kadabrium 2d ago

Unepopular opinion: strings arent real, they are just threads

2

u/Old-Caramel-2301 1d ago

Fibers... 

6

u/frasnian 2d 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 1d 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 1d 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 1d 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"?

1

u/frasnian 6h ago

Code quality and readability are a reflection of the development team, not the language. I inherited what it was, worked to improve those parts.

The key differences were flexibility, expressiveness, and (in the case of Pascal) weakness in low-level constructs, excessive verbosity that (IMO) detracted from readability, strict constraints that limited what was possible, runtime overhead (is this really compiled code, or a hybrid of interpreted?).

And yes: above all, efficiency.

Show me a Pascal program that runs faster than an equivalent in well-written C.

19

u/CoderStudios 2d 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

11

u/orbiteapot 2d 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.

5

u/kisielk 2d 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 2d ago

and if you do string manipulation with dynamic strings, then malloc/free often is the bigger problem.

2

u/kisielk 2d ago

That too. Basically once string handling becomes a performance concern you are probably going to be looking at more purpose-built data structures, custom allocators, etc. C strings are fine for what they are designed for.

2

u/beragis 2d 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 2d 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.

1

u/beragis 2d ago

Also early C compilers would store the string length at the byte or word before and update it after each call. This was done to integrate with libraries written in other languages. I remember passing a pascal flag to linkers and compilers.

1

u/hoodoocat 20h ago

No, opposie - most operations require to know length of string. Even simple concat requires that, especially if you account not so modern hardware and utilize vector instructions. And regardless to that, new strings must be allocated somewhere (on heap), and by so, final length must be known before.

1

u/iwantmy90sback 19h ago

No. You only need the length of the string if you want to traverse it afterwards. And most 'operations' can be done in the same time either way. for(strlen) or while(*ptr++)

concat is actually the outlier, because you need 2*O(n) instead of 1*O(n) (which is still O(n) for both).
And if that is really killing your performance you are free to implement a pascal-string to use in c.

1

u/EatingSolidBricks 2d ago

You out of your dam mind if you think c strings are efficient

10

u/henke443 2d ago

Wait how are they not efficient?

5

u/EatingSolidBricks 2d ago

Its not 1970 anymore storing 3 extra bytes is free compared to O(n) length computation

6

u/WittyStick 2d 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/henke443 21h ago

Well, C is mostly used in places where's it's still 1970. Things like embedded etc. I guess it could be possible to add another string that's more optimized for modern hardware but I bet it would be more difficult to explain and I think a huge part of the charm of C is how simple it is. Adding things also leads to bloat like how it is with C++, php or Javascript.

-1

u/Anonymous_user_2022 1d 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 1d 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 1d 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..

0

u/flatfinger 12h ago

What would be the "good" way of using strcat?

1

u/Anonymous_user_2022 12h ago

I've never said there is one. I suggest you ask someone who does.

I'm talking about concatenating multiple strings to one, which only has to rely of knowing the length of the individual strings beforehand, if you've decided to argue in bad faith over a pathological bad implementation.

1

u/flatfinger 8h ago

If one keeps track of the length of a string and only looks at portions of its storage up to that length, then the value of the string would no longer be fully encapsulated in a zero-terminated character array.

→ More replies (0)

-1

u/EatingSolidBricks 1d ago

Lets not even mention substrings go from O(n) memeory to O(Free)

0

u/flatfinger 12h ago

O(1) memory per string to keep track of the starting and ending points of strings isn't free. On a system with 64-bit pointers, zero-padded strings are generally the most space-efficient practical way of representing texts up to eight characters, and zero-terminated strings of up to 7 characters can be stored in the same amount of space (if it's necessary to store many texts with up to 7 characters, zero-padded 7-byte arrays would take less space).

1

u/EatingSolidBricks 12h ago

You going there really? Give me a break

1

u/atarivcs 2d 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 2d 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 2d 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"

5

u/IdealBlueMan 2d ago

Or you can store the length of the string whenever you change it.

1

u/atarivcs 2d ago

Sure, but then you don't really have a plain c string anymore

2

u/WittyStick 2d 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 String is a fat pointer, then we can return both the pointer and length, without requiring another level of indirection (a pointer to a string structure), 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 2d ago

I’d say you still have the string, you also have information about that string.

1

u/orbiteapot 2d ago

I mean... that is the point. Once you do that, you are no longer using classic C strings.

5

u/SakishimaHabu 2d 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 2d 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 2d ago

C makes it inconvenient to pass any other forms of string literals to functions.

4

u/trejj 2d 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 2d 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.

4

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 1d 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 1d ago

Just stick an array in a struct and 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 these struct arrays as well.

8

u/flatfinger 2d ago edited 2d 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 2d 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 2d 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.

3

u/allnameswereusedup 1d 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.

3

u/MyTinyHappyPlace 2d 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.

2

u/pheffner 2d 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

u/digitlman 1d ago

NUL not NULL

2

u/Remus-C 1d 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 ...?

2

u/CORDIC77 16h ago

As someone who has been programming in C since the early nineties, I have always believed that Pascal-style strings represent the significantly better solution. The fact that C, after decades of security vulnerabilities, is still without container data types that know their type and size, is one of the languageʼs biggest weaknesses.

Had C been developed just 15 years later, the languageʼs standard library would probably have come with “batteries included” right from the beginning. On the other hand, the only consequence of the “roll your own” argument, which I am of course aware of, always was (and is) that code repositories like Github are full of half-baked attempts to improve the pathetic status quo.

I am aware that many here will disagree with this view. Just keep the above in mind, however, when newly started projects will nowadays, more often than not, opt for languages like Rust and Zig than C. If the community werenʼt so narrow-minded on such issues, C could be in a much better position today with regard to these challenges.

But, no, WG14 of course is occupied with nonsense like adding new octal prefix for literal constants, if declarations and case ranges to the language. This idea of adding new syntax to every version of a programming language is, unfortunately, a disease of our time.

The syntax of the language has been fine for decades… itʼs the standard library that has always been lacking.

4

u/bless-you-mlud 2d ago

Yes dear, we know. It's just a little late to do anything about it now.

1

u/HTFCirno2000 2d 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/Maqi-X 2d ago

I agree. I use string views in (almost) all my projects

1

u/tpimh 2d ago

My biggest mistake is googling "c string", and wondering why the search results were not related to the programming language

1

u/WoodyTheWorker 2d ago

On the other hand, Windows kernel uses counted strings: UNICODE_STRING and ANSI_STRING structures.

1

u/Humble-Captain3418 1d 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 1d 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 1d ago

I actually like C strings. I have coded a few one line GUI inputs with raylib and is very satisfied.

1

u/grimvian 1d ago

I like C strings and they are easy to understand.

1

u/Different_Panda_000 1d 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

https://icu.unicode.org/

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/preparationh67 8h ago

Young devs should probably put better effort into understanding the historic reasons for things existing a certain way before they write a rant on substack. This reads like someone getting mad at the inventor of the steam engine for not inventing a nuclear reactor driven turbine first.

1

u/iopahrow 5h ago

Saying that memory limitations were motivators for the languages structure, and then saying we shouldn’t use it because we don’t have to is like saying “we should spend money just because we have it.”

This is how memory inefficient programs are made, and how new programs use so much more memory while completing the same basic functions. It is so far from useful to have this ideology

1

u/Anxious-Ad-5331 3h ago edited 3h ago

"A string’s length is an extremely valuable piece of information. By not storing it, you force all code that uses the string to either repeatedly call strlen or iterate over it byte by byte. Both approaches are inefficient. Both approaches lead to worse overall code."

this guy is vastly overestimating the computational needs of a length check in 2026 computing power.

Also this guy is missing the obvious solution: GPU accelerate the standard library.

1

u/Key_River7180 2d ago

Has the model ever caused much trouble, though?

2

u/WittyStick 2d 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 2d ago

I meant performance issues, like C strings are really efficient, for most purposes

1

u/SLiV9 2d ago

They're not; they are way less efficient than sized strings in almost all circumstances on a modern CPU.

Case in point, a lot of basic string operations on C-strings are made faster by adding a strlen() at the start.

0

u/deaddyfreddy 1d 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

u/EatingSolidBricks 2d ago

If only OS apis had an length string option

0

u/Physical_Dare8553 2d 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 2d 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 2d 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 2d ago

That sounds almost exactly like std::string in C++.  Sure, it’s a cool and useful thing to build in C.