
22nd Century C - Procedural
https://procedural.github.io/c22/
======
mieko
I've spent two decades writing C and C++, but the last 8-9 years in really
high-level languages (Ruby, Javascript, Python). From either end of the
spectrum, I've never felt the need for such emphasis on fixed-sized numeric
types.

I've commonly needed _access to_ fixed size numerics, like when sending
texture formats to the GPU, defining struct layout in file formats and network
protocols, but I have never once thought: "You know what, I'd like to make a
decision as to the width of an integer every time I declare a function."

"Just has to work" low level code was the norm during the 16-bit to 32-bit
transition, so it was a fucking pain. Notice how smooth the 32-bit to 64-bit
transition went? (and yes, it was smooth.) I credit that to high-level
languages that don't care about this stuff, and people using better practices
like generic word-sized ints and size_t's in lower level code. Keep that stuff
on the borders of the application.

I've noticed a decent-sized emphasis on type size in both Crystal and Swift,
two not-entirely braindead newer languages. I don't get it, it's a big step
backward.

~~~
tbirdz
C's normal integer types work the closer to the way you want. When you say
you're returning an int, you're not really specifying the exact size at all.

For those who don't know, C's char, short, int and long data types aren't
really "fixed size". The standard defines their order: sizeof(char) <=
sizeof(short) <= sizeof(int) <= sizeof(long), but the actual size of these
integer types depends on the platform and compiler. If you want to use fixed
size ints, you'd need to use the uint8_t, uint16_t, uint32_t types from
stdint.h. Although interestingly enough C99 doesn't require the platform to
provide these types, it only mandates the uint_least_n_t and uint_fast_n_t
types (n=8,16,32..) which are guaranteed to be at least, but not necessarily
exactly the requested width.

Now you may be thinking "sure the standard says they could be different, but
they aren't really, a char is 8bit, a short is 16-bit an int is 32-bit a long
is 64-bit". So here's a couple examples for you: many DSP platforms have a C
compiler where char is 16-bit. Also Microsoft Visual c++ on windows on x86_64
compiles longs as 32-bit while GCC on Linux on x86_64 compiles longs as
64-bits.

>Notice how smooth the 32-bit to 64-bit transition went? (and yes, it was
smooth.)

I know you said it was smooth, and maybe it was for some applications. But for
many others, the 32-bit to 64-bit transition actually caused a lot of
problems! Andrey Karpov has already done a great job of categorizing many of
them, so I won't waste my time repeating him, but you can read his list here:
[http://www.viva64.com/en/a/0065/](http://www.viva64.com/en/a/0065/)

~~~
mieko
Yeah, I'm arguing that the situation you describe (accurately) is better than
baked-in sizes all over the source code.

Use the platform-native types (whatever size they may be) unless you have a
reason not to, then use <stdint.h> or an analogue. If CHAR_BIT is 13, let char
be 13 bits: the platform probably chose that for a reason. When you have to
pack it into a TCP header, do your strict fixed-width stuff there.

~~~
tbirdz
I completely agree with you when it comes to function return types.

For data structure fields, especially structs used many, many times like in
very large arrays, I'd say it's sometimes worth using fixed size types to get
better control over memory use. Using a 64-bit int for a field a 16-bit
integer can handle will use up 4x as much memory. And if you've got a ten or a
hundred million structs of that type, then it really adds up.

For example size_t is 64-bit on my x86_64 system. But modern x86_64 systems
can only use 48-bit address spaces, so a 64-bit sized object can't even be
addressed! Even worse is my cpu and motherboard have a 32gb maximum of RAM
(for an effective 35-bit physical addressing limitation). And size_t is
supposed to be able to store the size of any object in memory, but on this
platform it stores things that won't fit in memory. So most of these 64-bits
are wasted on modern systems. For files you can use off_t if you're on POSIX,
but the C standard doesn't say anything about requiring size_t to be able to
store any filesize in a filesystem.

Just using size_t is not enough to make your code work correctly on 64-bit
size quantities either. For example, you make your strlen implementation
return size_t, and you use size_t everywhere you do anything with a string.
But can your application really handle strings that are bigger than the system
RAM , or the hardware address space? Are your algorithms even efficient enough
to handle the 4,294,967,295 byte maximum string size for a 32-bit system?

The effort to get your program to work efficiently on >32-bit quanities is
often much harder than just using size_t instead of int.

So to me, when I see 64-bit size_ts being used everywhere in code that won't
actually be able to handle working with >32-bit quantities, it just feels a
little useless. Of course this is really more a complaint about how big size_t
is on the x86_64 platform than it is a complaint about the idea of size_t in
general. If only we had 48-bit size_ts (24-bit would be handy too!)

~~~
gcr
I strongly disagree with your sentiment.

If you wrote an application that's as efficient as possible without any wasted
bits in the size_t type, it then only works on your machine.

If I wanted to run such an application on my supercomputer with 2TB of RAM
(such machines exist), I would then have to recompile for a 41-bit size_t.

We use machine-neutral (but architecture-specific) size_t for these kinds of
things explicitly to avoid recompiling on different machines that are
instances of the same platform.

Said another way, binary distributions _could not exist_ if everything was
made efficient for the underlying hardware. It would stink to have to
recompile the world after upgrading RAM.

I'd rather have a few bits of wasted space (which are typically lost anyway
due to struct packing) than lose intra-platform comparability.

~~~
tbirdz
A 48-bit size_t like I suggested could address up to 256TB of RAM. All modern
x86_64 cpus are limited to 48-bits of address space, you're not losing any
portability here.

Also consider my strlen example. Say you compute strlen by iterating through
the whole string until you find a 0, then you return a size_t for the number
of bytes you iterated through. That operation is O(n) in the length of the
string. If you were to use my strlen function on a string whose length is
greater than would fit into a 32-bit integer, say a 1TB string or something,
then the function would take so long to compute it that it would be useless.
To be efficient, you'd probably have to redesign your program to do some
special things to handle 1TB strings, maybe some special algorithms, or some
kind of indexing. Returning a 64-bit integer type does not mean that the
function can actually handle working with 64-bit sized quantities. So if you
have a lot of datastructures where you keep string length, why store them as a
64-bit size_t when your application would be completely unable to handle
strings of that size without keeling over?

Of course you wouldn't really use a 48-bit size_t, because x86_64 cpus don't
work well with 48-bit quantities.

------
cperciva
You can write C in any language. You can also write any language in C.

The fact that it's possible does not imply that it's a good idea. Some of the
macros here are in common use (e.g., countof, although often with other
names); some will make experienced C developers say "what's this? Oh, you mean
<insert expansion here>; why did you use a weird macro?"; and some, like the
redefinitions of _case_ and _default_ are actively hostile and are guaranteed
to result in bugs when exposed to experienced C developers.

For more of the same, see "things to commit just before leaving your job":
[https://gist.github.com/aras-p/6224951](https://gist.github.com/aras-p/6224951)

------
generic_user
I'm not sure that 'address' is desirable.

    
    
        int adress adress adress foo;

vs

    
    
        int * * * foo;
    

Double pointers are extremely common and triple pointers show up now and then.

Also '__attribute__ cleanup' as far as I know only works with automatic
variables that live on the stack. Mucking about with code execution and
automatic variables as the stack unwinds is not a style I would like to see in
general purpose C coding. Its the one type of automatic memory management that
you get for free and can't really screw up.

The rest of the macro loops and so forth seem fairly standard. Its good to
experiment and explore what you can do with the compiler and the preprocessor.

~~~
noobermin

       int double_address foo; /* ? */
    

Or is it _n_ steps away from ProducerObserverFactoryStragedy?

~~~
generic_user
Actually, its not even an address its a pointer.

To get the address of an object you use the unary 'address of' operator '&'.

His macro was #define address *

The * is a pointer. You can declare a variable of type pointer to T with as
many indirections as you want. int * * * * * * foo is fine. But you also use
it to deference your object. bob = * foo;

You can't practically have a different name for each level of indirection to
what ever the IS0 standard/Compiler limit is for * .

Also you end up with nasty indirections like this.

    
    
        **(*((struct foo*)(*bob.x))).y
    

Its nasty enough as it is without trying to name each pointer indirection.

------
dikaiosune

        fn main() {
            println!("hello, world");
        }
    

I kid, I kid. This looks cool!

------
alxmdev
Very cool! I wasn't aware of the cleanup attribute, can't wait to try it out:

 _The cleanup attribute runs a function when the variable goes out of scope.
This attribute can only be applied to auto function scope variables; it may
not be applied to parameters or variables with static storage duration. The
function must take one parameter, a pointer to a type compatible with the
variable. The return value of the function (if any) is ignored._

[https://gcc.gnu.org/onlinedocs/gcc/Common-Variable-
Attribute...](https://gcc.gnu.org/onlinedocs/gcc/Common-Variable-
Attributes.html)

~~~
jagger11
FYI - here's defer implementation for both gcc/clang -
[http://pastebin.com/EXZuRAdT](http://pastebin.com/EXZuRAdT)

------
microcolonel
The "case" and "default" overrides seem kinda dicey.

~~~
Tim61
Yeah, that is probably what bothers me the most about this. I write C code
almost everyday. I can't remember the last time I forgot a "break", but I do
use fallthrough when it simplifies the logic. It also breaks even the simple
case of having multiple labels for the same code.

Also, the for-loop overrides are ugly and pointless, IMO.

------
em3rgent0rdr
I don't get it. Could someone summarize/explain this.

~~~
pqhwan
I'm only guessing, but it looks like a C header file with a set of macros that
produces neat-looking C code. Not sure what's the deal with the 22nd century
thing.

~~~
PieterH
It's a clever joke and rather cynical deconstruction of our industry. The
author suggests that in 100 years the C language will not only still exist and
be widely used, it will have advanced almost nothing at all due to the
industry's inability to solve real problems and instead focus on virtue
signalling languages such as Clojure and Ruby. The author hammers home his
point by demonstrating how a simple header file can at once render the code
unreadable, while adding absolutely no value at all. I think he or she is
specifically talking about Silicon Valley here, though of course since the
joke is expressed in code, one can only guess.

Seriously, I'd hope in 2100 we'd have pushed our systems languages beyond
facile keyword redefinition. Maybe even towards concurrency and an actor
model. Like I explored in my unfinished (sorry, shit happened) book "Scalable
C".

Or, Rust.

------
jdmoreira
Maybe the author wanted this to be C99 compliant but a cool thing is that by
using clang we even have lambdas. It's called blocks, the same as in
Objective-C.
[http://clang.llvm.org/docs/BlockLanguageSpec.html](http://clang.llvm.org/docs/BlockLanguageSpec.html)

I've been using them in all my new C code, since I'm stuck to clang anyway,
and it's awesome.

------
TickleSteve
this is basically an updated version of iso646.h

[[https://en.wikipedia.org/wiki/C_alternative_tokens](https://en.wikipedia.org/wiki/C_alternative_tokens)]

...and no, they're not a good idea, its just a syntactic change. If you want a
'better-C', language-wise, Rust is pretty much best current hope, tho that
wont mature for another decade or so.

~~~
ci5er
Really? I haven't had a chance to dig into it and a quick google-based look-
see didn't pop up immediately obvious results, but I would have thought that
two areas where one would want to use C would include specific bit
manipulation and explicit memory management. Both of those violate the high-
level principles that I thought Rust was supposed to be about. So, either I
was wrong (which happens more and more these days), or Rust has a work-around,
or it's not a good fit for that last bastion of C use-cases.

Thoughts?

~~~
kbaker
Sorry, you should do more research... :) Rust has great support for very low
level operations. Maybe search for 'bitwise operators' or 'inline assembly'.
Memory management is even _more_ explicit and strict than C, since you also
have to deal with lifetimes and borrowing.

The best thing is, though, since Rust is such a young language, you can change
it! Imagine trying to add a new language feature to C, something like the ?
operator currently being evaluated for addition in Rust. It would take years,
if not decades, to get those things in C. Even now MSVC is just rolling out
support for C99.

I can definitely see Rust taking over C's current place in the market over the
next decade or two. Better memory safety, higher-level constructs to improve
productivity, more cross-platform compatibility, and modern libraries to deal
with things like Unicode are a _huge_ draw.

~~~
ci5er
Sounds like I should!

Thanks!

------
transfire
Still using header files in the 22nd century?

------
n_yuichi
It reminds me of libCello

[http://libcello.org](http://libcello.org)

------
dpc_pw
That is very impressive. I really like the cleverness and cleanliness of these
"hacks".

Still... I wouldn't introduce it into existing/shared codebase due to risk of
confusing everyone, and I will never start my own project in C again.

------
jagger11
I wonder if this is in any way inspired by this -
[https://github.com/google/honggfuzz/blob/master/common.h](https://github.com/google/honggfuzz/blob/master/common.h)
?

I use there defer for both gcc/clang, countof(arr) -> ARRAYSIZE(array)

In any case, yeah, going through gcc/clang internals, and through C11
standards gives people some ways to speed-up and clena-up their
implementations a bit.

------
catb0t
It looks like just a cross between Go, DLang and maybe something else. Maybe
I'll use it to troll my programming teacher.

------
approachingtraj
This is so fucking stupid.

------
Mathnerd314
Too little too late. Rust is already taking over, by the 22nd century C will
be dead.

~~~
Animats
There will probably be C code running in 2100. That's only 84 years away.
FORTRAN is now 60 years old and still going strong in scientific computing.
(Partly because, in most other languages, multidimensional array support is
worse.)

I suspect, though, that new successful languages will have automatic memory
management, either GC/reference counting or Rust-type borrow checking. There's
no reason for a new language with the lack of safety of C.

~~~
petre
Dlang is actually quite nice and mature, although nowhere nearly as hyped as
Rust, Go or Swift. You get a GC with the option of deactivating it and doing
the memory management yourself, pointers and casts if you need them (you
usually don't), an auto type, fast compiling times, among many other cool
things.

[https://dlang.org/overview.html](https://dlang.org/overview.html)

