Libc string functions don't manage memory. They can be used no matter where your strings are stored. It is more of a choice between generality vs convenience in common cases.
Passing an allocator (like Zig) or a container (like in my framework) to anything that needs to allocate some memory to store a result is both explicit, low overhead and quite convenient in practice.
I've been musing for a while now: what would it look like if we were to discard the C library and design a new one, leaving the language itself intact?
C is not perfect, there are some parts of the syntax that I strongly dislike, like casting or function pointers declaration...
But it is overall a good enough syntax, much simpler than C++.
You often end up with some kind of structure, or variations of structures, for strings:
struct string {
size_t length;
char data[];
};
struct string {
size_t length;
size_t alloc;
char *data;
};
Those are just examples. The tricky part is figuring out the different ownership use cases you want to solve. Because C gives you so much freedom and very little in the standard library, you end up with a lot of variations. You might use reference-counted strings, owned buffers, or string slices, etc. You might want certain types to be distinguished at compile-time and other types to be distinguished at run-time.An example can be found in the Git source code.
https://github.com/git/git/blob/master/strbuf.h
The history of changes to this file is interesting as well. This is a relatively nice general-purpose string type—you can easily append to it or truncate it.
I've seen many libs using this style of strings, not convinced by the practicality.
If you’re not convinced of the practicality, it sounds like you are simply not convinced of the practicality of doing string processing in C at all, which is a fair view point. String processing in C is somewhat a minefield. Libraries like Git’s strbuf are very effective relative to other solutions in C, but lack safety relative to other languages.
The trick is to pass an allocator (or container) to string handling functions.
If/when I want to get rid of all the garbage I reset the container/allocator.
I’ve seen similar approaches, e.g. with APR pools, and if your application can work within those restrictions, it’s very convenient.
Building blocks for memory were also very different from stdlib, notably the use of Handles, which were pointers of pointers, so that the OS could move a block of data around to defragment the heap behind your back without breaking the memory addressing.
C++ string_view is closer to the Right Thing™ - a slice, but C++ doesn't (yet) define anywhere what the encoding is, so... that's not what it could be. Rust's str is a slice and it's defined as UTF-8 encoded.
Back then it wasn't clear which encoding method would turn out to be dominant, so we did all three. (Java was built on UTF-16.)
As it eventually became clear, UTF-8 is da winnah, and the other formats are sideshows. Windows, which uses UTF-16, is handled by converting UTF-8 to -16 just before calling a Windows function, and converting anything coming back to UTF-8.
D doesn't distinguish between a string and a string view.
Imagine you go to a library and insist on borrowing "My Cousin Rachel", but they don't have it. "Oh I don't care whether you have the book, I just want to borrow it" is clearly nonsense. If they don't have it, you can't borrow it.
> D doesn't distinguish between a string and a string view.
In C++ std::string owns the buffer and std::string_view borrows it. If there is no difference between the two in D, then how is this difference bridged?
They added a setting in Windows 10 to switch the code page over to utf-8 and then in Windows 11 they made it on by default. Individual applications can turn it on for themselves so they don't need to rely on the system setting being checked.
With that you can, in theory, just use the -A variants of the winapi with utf-8 strings. I haven't tried it out yet as we still support prior Windows releases but it's nice that Microsoft has found a way out from the utf-16 mess.
I don't mind seeing UTF-16 fade away. We've been considering scaling back the D support for UTF-16/32 in the runtime library, in favor of just using converters as necessary. We recommend using UTF-8 as much as practical.
And the wheels fall off with the first string longer than 255 characters.
However, Free Pascal has the worst documentation of any major project I've ever encountered (The exact opposite of Turbo Pascal), so I can't link to a good reference. Their Wiki is a black hole of nuance and sucks all useful stuff off the internet.
I believe this is used by Redis.
A Freudian slip, methinks.
my_function(my_var, 3.6, "bzarflo", my_other_var, false);
The string handling functions are part of the story, but the null-terminated char * is produced when the compiler reaches a string literal, and writing code without being allowed to just use string literals when it's convenient tends to feel like coding with oven mitts on. my_function(my_var, 3.6, $("bzarflo"), my_other_var, false);
Isn't that much more of a mouthful, and as long as 'my_function' knows to free it, then you're A-OK! The only trouble is '$()' isn't legal in standard C, so a real solution would have to be something like 'str()'.[1] https://www.digitalmars.com/articles/C-biggest-mistake.html
float m[10][10];
it not a an array of pointers, but a 2D dimensional array with 2D memory layout.
int[][] jagged; // an array of `int[]` (i.e. each element is a pointer to a `int[]`)
int[,] multidimensional; // a "true" 2D array laid out in memory sequentially
// allocate the jagged array; each `int[]` will be null until allocated separately
jagged = new int[][10];
Debug.Assert(jagged.All(elem => elem == null));
for (int i = 0; i < 10; i++)
jagged[i] = new double[10]; // allocate the internal arrays
Debug.Assert(jagged[i][j] == 0);
// allocate the multidimensional array; each `int` will be `default` which is 0
// element [i,j] will be at offset `10*i + j`
multiDimensional = new double[10, 10];
Debug.Assert(multiDimensional[i, j] == 0); int N = 10;
char buf[N] = { };
auto x = &buf;
and 'x' has a slice type that automatically remebers the size. This works today with GCC / clang (with extensions or C2X language mode: https://godbolt.org/z/cMbM57r46 ).We simply can not name it without referring to N and we can also not use it in structs (ouch).
How is this not a quality of implementation issue? Any implementation is free to track all sizes as much as they want with the current standard.
Either a implementation is forced to issue an error at run time if there is an out of bounds read/write and in that case its a very different language than C, or its feature as-if lets any implementation ignore.
https://godbolt.org/z/qh7P93Tcd
And I agree that this is a misuse of auto. I only used it here to show that the type we miss already exists inside the C compiler, we simply can name it only by constructing it again:
char (buf)[N] = ...
but we could simply allow
char (buf)[:] =
and be done (as suggested by Dennis Richtie: https://www.bell-labs.com/usr/dmr/www/vararray.pdf)
...and has been since most of us ever used C.
I think one of the major failings of C was the lack of a good standard library that updated with the times.
Actually, I believe a rich standard toolbox was one of the best features of python, and helped with its success.
(Or maybe I’m misinterpreting your comment?)
First part of my comment relates to C library in general, second part of my comment refers to strings and arrays, even if not explicitly.
unsigned int str_len(const char *s)
{
register const char *t;
t = s;
for (;;) {
if (!*t) return t - s; ++t;
if (!*t) return t - s; ++t;
if (!*t) return t - s; ++t;
if (!*t) return t - s; ++t;
}
}
I only do "trivial" work with strings but I still use this instead of stdlib strlen. Meanwhile I use software everyday that I know uses POSIX strlen.NB. I am not the author; this is in the public domain. The author is djb.
- This function will return the number of bytes, not of characters or codepoints. - str and len are both abbreviations, we should use full words when possible - We can also be more explicit about what the function does, it does not simply returns the string length, it counts characters (or bytes in this case)
Here is how I would name it:
u32 CountBytesInString(char* string); u32 CountCharactersInString(char* string);
And on the implementation side, this work can be done with SIMD instructions, and be really freaking fast, but still, it should be explicit for the user that the work is O(n) complexity, not exactly free.
u32 and char are abbreviations.
strlen is also pretty unambiguous, but I still have to check what strstr means.
- Makes it clear that this returns the number of bytes, assuming a naming convention where `sz` refers to size (in bytes) and `ln` refers to length (in some other unit which would be specified in the type). Note that in C, 'characters' refers to bytes. It's a flaw in how C names its types, yes, but I wouldn't say it should be any different just because other languages do things differently.
- It doesn't use full words because I don't think it needs to. Abbreviations are OK as long as every (invested) party agrees that they're sane, and I think they're pretty sane.
- It makes it explicit that it is performing a calculation (hence, is O(n)) via 'get'.
I don't think all this is necessary, though - I actually think 'strln()' is enough. First, because characters means bytes, I can assume that this function is getting the number of characters (bytes) in a string. I wouldn't expect it to give me anything else! Second, in C, if strings were a struct of some sort, I'd expect to be able to get their length via 'str->ln', which would be O(1). The fact that the length is found through a function in the first place signals to me that it's doing something behind the scenes to figure that out. Remember - that's just my opinion, which I admit is extreme - but I think yours is just as extreme.
I think that full words and verbs are easier to read and avoid ambiguity.
I guess this is a matter of style and preference.
This anecdote reminds me of the Mutazt type, something I found in a new codebase I was asked to debug. I had to dig for almost an hour to find exactly what this type was.
Turns out it was a char*, a C string. Buried under 4-5 levels of abstractions.
Mutazt = Mutable ASCII Zero Terminal.
This sort of practice is very outdated. The last time it made sense for performance reasons was probably the early 90s or earlier.
Additionally, strlen is not one of the C string functions you want to replace due to defects, the same way you would want to do with crusty old strcpy. If you're working with C strings there is nothing wrong with strlen. (Just don't call it redundantly in a loop body ...)
Default libs are sometimes very optimized but very often they are not, unfortunately.
If you care about performance, you should not rely blindly on the the defaults.
A long time ago I wondered about the performance of memcpy on the Nintendo DS, for sure they would have provided a hand optimized version? And yes, it was handcrafted ARM assembly code, but my own version turned out to be twice as fast.
They simply forgot to use a simple prefetching trick in their implem.
I'm going to have to doubt the start of your comment. Having seen a lot of libc implementations I think you are better off not wasting time optimizing strlen. Also memcpy, probably memcpy moreso. Most memcpy()s I've seen in the current century are using SIMD instructions and the like. And compilers don't even bother emitting a call to libc for it anymore, they do it as a builtin.
I got the prefetching trick from Intel source code, except that I replaced the PLD instruction by a simple dummy load.
And about strlen, you'd be surprised, some implems are very good, and some are not, depends on the compiler and the library. I've ran benchmarks, I was surprised too.
To be honest, I don't really need super fast strlen, but I was curious and also learning to write fast SIMD code, basic string handling is a nice exercise.
By contrast, many more people are updating optimizations to GCC or clang for arm, more frequently and over a longer timeframe.
GCC and Clang are very nice compilers, but they are a different thing than the std lib. glibc, musl, the Windows C Runtime, iOS, Android, all have different implementations, sometimes outdated.
Same is true of MSVC.
It's been that way on most modern compilers for about 20 years.
That's why I'm saying rolling your own may be futile. Compilers, not just libc, have paid a lot of attention to getting those things fast.
But testing is always there to the rescue, and these days we have Godbolt.
But I have not published those benchmarks, if this is what you're asking, the Nintendo thing I am afraid I cannot reproduce easily as I no longer have this devkit on hand.
About the strlen benchmark, this is something I've done a few years ago, that could be easy to run again, but I am not sure this is worth the effort just to convince a random dude on the internet...
Yes. Share the data or you're spreading FUD.
https://git.musl-libc.org/cgit/musl/plain/src/string/strlen....
#include <string.h>
#include <stdint.h>
#include <limits.h>
#define ALIGN (sizeof(size_t))
#define ONES ((size_t)-1/UCHAR_MAX)
#define HIGHS (ONES * (UCHAR_MAX/2+1))
#define HASZERO(x) ((x)-ONES & ~(x) & HIGHS)
size_t strlen(const char *s)
{
const char *a = s;
#ifdef __GNUC__
typedef size_t __attribute__((__may_alias__)) word;
const word *w;
for (; (uintptr_t)s % ALIGN; s++) if (!*s) return s-a;
for (w = (const void *)s; !HASZERO(*w); w++);
s = (const void *)w;
#endif
for (; *s; s++);
return s-a;
}https://cr.yp.to/ucspi-tcp/install.html
https://cr.yp.to/daemontools/install.html
https://cr.yp.to/djbdns/install.html
https://sourceware.org/git/?p=glibc.git;a=blob_plain;f=strin...
#include <libc-pointer-arith.h>
#include <string-fzb.h>
#include <string-fzc.h>
#include <string-fzi.h>
#include <string-shift.h>
#include <string.h>
#ifdef STRLEN
# define __strlen STRLEN
#endif
/* Return the length of the null-terminated string STR. Scan for
the null terminator quickly by testing four bytes at a time. */
size_t
__strlen (const char *str)
{
/* Align pointer to sizeof op_t. */
const uintptr_t s_int = (uintptr_t) str;
const op_t *word_ptr = (const op_t*) PTR_ALIGN_DOWN (str, sizeof (op_t));
op_t word = *word_ptr;
find_t mask = shift_find (find_zero_all (word), s_int);
if (mask != 0)
return index_first (mask);
do
word = *++word_ptr;
while (! has_zero (word));
return ((const char *) word_ptr) + index_first_zero (word) - str;
}
#ifndef STRLEN
weak_alias (__strlen, strlen)
libc_hidden_builtin_def (strlen)
#endif
NetBSD common strlen:https://ftp.netbsd.org/pub/NetBSD/NetBSD-current/src/common/...
#include <sys/cdefs.h>
#if defined(LIBC_SCCS) && !defined(lint)
#if 0
static char sccsid[] = "@(#)strlen.c8.1 (Berkeley) 6/4/93";
#else
__RCSID("$NetBSD: strlen.c,v 1.3 2018/02/06 09:28:48 mrg Exp $");
#endif
#endif /* LIBC_SCCS and not lint */
#if !defined(_KERNEL) && !defined(_STANDALONE)
#include <assert.h>
#include <string.h>
#else
#include <lib/libkern/libkern.h>
#endif
size_t
strlen(const char *str)
{
const char *s;
for (s = str; *s; ++s)
continue;
return(s - str);
}
Apple strlen:https://opensource.apple.com/source/Libc/Libc-1244.50.9/stri...
Apple strlen comes from FreeBSD. Until 2009, FreeBSD used an unoptimised strlen.
https://svnweb.FreeBSD.org/base/head/lib/libc/string/strlen....
FreeBSD eventually copied^1 NetBSD's x86_64 strlen.
1. "modeled after", "inspired by" or whatever words the HN commenter prefers to use.
https://svnweb.FreeBSD.org/base?view=revision&revision=18770...
#include <sys/cdefs.h>
__FBSDID("$FreeBSD$");
#include <sys/limits.h>
#include <sys/types.h>
#include <string.h>
/*
* Portable strlen() for 32-bit and 64-bit systems.
*
* Rationale: it is generally much more efficient to do word length
* operations and avoid branches on modern computer systems, as
* compared to byte-length operations with a lot of branches.
*
* The expression:
*
* ((x - 0x01....01) & ~x & 0x80....80)
*
* would evaluate to a non-zero value iff any of the bytes in the
* original word is zero. However, we can further reduce ~1/3 of
* time if we consider that strlen() usually operate on 7-bit ASCII
* by employing the following expression, which allows false positive
* when high bit of 1 and use the tail case to catch these case:
*
* ((x - 0x01....01) & 0x80....80)
*
* This is more than 5.2 times as compared to the raw implementation
* on Intel T7300 under EM64T mode for strings longer than word length.
*/
/* Magic numbers for the algorithm */
#if LONG_BIT == 32
static const unsigned long mask01 = 0x01010101;
static const unsigned long mask80 = 0x80808080;
#elif LONG_BIT == 64
static const unsigned long mask01 = 0x0101010101010101;
static const unsigned long mask80 = 0x8080808080808080;
#else
#error Unsupported word size
#endif
#define LONGPTR_MASK (sizeof(long) - 1)
/*
* Helper macro to return string length if we caught the zero
* byte.
*/
#define testbyte(x) \
do { \
if (p[x] == '\0') \
return (p - str + x); \
} while (0)
size_t
strlen(const char *str)
{
const char *p;
const unsigned long *lp;
/* Skip the first few bytes until we have an aligned p */
for (p = str; (uintptr_t)p & LONGPTR_MASK; p++)
if (*p == '\0')
return (p - str);
/* Scan the rest of the string using word sized operation */
for (lp = (const unsigned long *)p; ; lp++)
if ((*lp - mask01) & mask80) {
p = (const char *)(lp);
testbyte(0);
testbyte(1);
testbyte(2);
testbyte(3);
#if (LONG_BIT >= 64)
testbyte(4);
testbyte(5);
testbyte(6);
testbyte(7);
#endif
}
/* NOTREACHED */
return 0;
}
It is not that difficult in practice.
The old C std lib is, in my opinion, outdated, obsolete and a very bad fit for complex string handling, especially on the memory management side.
In my own framework, the string management module is using a dedicated memory allocator and a "high level" string API with full UTF8 support from the start.
As a general rule, I think that the C std lib is the weakest part of the C language and it should only be used as a fallback.