The Assembly like abilities have been growing as language extensions in specific compilers, not as part of ISO C.
Going back to K&R C, inline Assembly or intrisics were not even available, all of that required using the Assembler directly.
The Assembly like abilities have been growing as language extensions in specific compilers, not as part of ISO C.
Going back to K&R C, inline Assembly or intrisics were not even available, all of that required using the Assembler directly.
It's a myth that this is a myth.
Have you ever worked with newbies learning C?
These students split pretty hard into two camps (of course there are oddball exceptions): those who knew assembly and find C easy, and those who didn't and struggle with pointers until they finally get it (some, never do).
Of the mainstream languages, C is the only one where understanding and dealing with direct memory access is a fundamental requirement if you're going to get anything done.
The myth argument is that C code doesn't directly translate into the execution flow on the CPU. No, of course it doesn't. That's not the point.
What people mean when they say C is lower level than most mainstream languages is because it forces you to deal with details most other languages paper over.
Yes, multiple languages have some way of achieving this kind of memory access, but except from C, it is considered an esoteric edge case that mostly nobody needs.
That's exactly what trips up people coming from higher level languages!
If you think of it in terms of objects and algorithms (linked lists etc) it is basically the same thing, so it feels like the same thing until things break in ways that make no sense (to people coming from high level languages).
To someone coming from assembly, you just tell them a pointer is like a register holding a memory address and everything is immediately obvious.
(I came to C from assembly so that was my experience)
But people coming from high level languages expect the semantics that a pointer is somehow bound to an object or data structure, which it can pretend to be, but ultimately it's just a register holding a memory address. So it can point to the first byte of the memory space holding your object, or it can point to the middle of it or to any random spot in memory. Or you can reinterpret what structure you pretend it is pointing at just by recasting. And so on.
A reference can only point to an object or an array on the heap. Array indexing is bounds-checked.
It seems obvious to me that these restrictions make references easier to reason about than C-style pointers, and therefore easier to learn.
I came to C from Assembly, so maybe I'm in a different camp, but references seem to me to be more difficult to reason about than pointers. Pointers just are, like math; references seem like a bunch of semi-arbitrary conventions.
Like the difference between writing SQL and using an ORM, which is supposed to be simpler but ends up being messier.
But it is also wrong to reduce a language to what is in the spec.
Apparently reducing the language to what is in the spec is only a thing when talking about C and to some extent C++.
When other languages have compiler specific extensions beyond the spec, it is a failure in their design.
Yet when C and C++ devs have to reach out to compiler specific extensions, it is not a design failure like it is pointed out to others, rather an advantage.
It is also wrong to not apply the same measure when it doesn't suit the message.
The point is in C you have greater control of execution and resources, not that it matches the hardware exactly. It’s a spectrum and C is closer on that spectrum than JavaScript.
So I keep re-educating folks that isn't the case.
Every thread has different people reading it, so there is always a first time for many of them.
And if you move to e.g. C# / Java or similar, if you squint, and you try to be a smart-arse, then you could deny that C is closer to the hardware than C#, because C# probably has everything you need to control it, to the same degree that C allows you to. But if you work in these languages for a while, and look at the code that you ended up producing, then again you will absolutely find that it would be ridiculous to not admit that C gives you better control.
And you could even extend this to Rust, because the language encourages you to use high-level prefabricated components. It discourages you from doing low-level things, at least a little bit I think (I'm not a Rust user).
I think what you are doing all the time, is you are being a smart-arse, nothing else. What interesting low-level performant things have you actually programmed lately?
I disagree somewhat. C gives you better control, and you have to accept that gift to get anything done. The likes of (modern) C# give you better control, but you can reject the gift if you want, and program in higher abstractions. You can also accept it in some places and reject it in others.
With C, you can reject the control, too, but then, you have to use third party libraries (or write them yourselves), and using those, your code looks less nice because it cannot escape C’s syntax (yes, macros help a bit there, but having real syntax beats it)
I have doubts that you can program easily in a C-style way in C# without adding lots of annotations everywhere in many places. But don't know, maybe I'm wrong, I did a search for a simple C-style arena allocator in C#, and it looked acceptable, it was quite close. The most annoying thing was maybe keyword boilerplate.
And then coming with such lengthy ad hominem.
Let make a fun exercise for the audience, given your performance remark.
Paste a random C code that I should replicate in whatever language I feel like.
There is one rule.
If the sample code is pure ISO C, then I will only use what is in the standard of whatever language I pick up.
If the sample code makes use of single language extension not part of ISO C, then I will have the freedom to also pick whatever language extensions I feel like.
What about you do xxHash? Should be quite basic, not a lot of complicated structures. https://github.com/Cyan4973/xxHash/blob/dev/xxhash.h
Or what about you do an audio or video codec? Or an operating system?
Not going to paste any of my own code, because any non-trivial stuff is hundreds to thousands of lines. But one more example (that I recently did myself): Create a block allocator (power of two blocks) with bookkeeping in shadow memory (administered in individually committed zones representing virtual memory regions of 64 MB (2^26)). Any used memory has bookkeeping support for being sub-allocated at any and all levels up from 64 KB (2^16) to 64 MB (2^26), and even higher (by joining committed regions). Individual blocks are collected (using intrinsic linking, because no memory allocation) in a hierarchy of pools of same-sized chunks that have the same parent, and can be recursively sub-allocated on any smaller chosen power-of-2 level, and finally be consumed in linear fashion (arenas). Blocks are pooled with a moderate retain policy (watermark system) to allow subsystems to almost completely avoid any system calls and avoid inter-thread synchronisation. The memory overhead must be below 1% even though it's totally flexible (as said has metadata for all levels from 64 KB up).
The bookkeeping should function on 32-bit systems (small virtual space, occupancy range from megabytes to 3 GB) as well 64-bit systems (2^48-2^57 bytes of virtual address space, occupancy range from megabytes to hundreds of gigabytes) with reasonable overhead compared to actual usage.
This requires intrusively linked lists, occupancy bitmasks, bit-counting and bit-prefix counting, OS syscall access (virtual memory), pointer arithmetic (alignment needed to address shadow bookkeeping memory) and thread synchronisation. The reference code is >> 95% pure ISO C++11 (could be C99 with few changes), with a little platform code glued in. It works on Windows but it could be ported to Linux in a few hours. It supports a mostly-immediate-mode GUI with hundreds of thousands (maybe millions?) of small variable-sized allocations per second. Allocation has almost completely disappeared from the CPU profile, well below 1% of CPU usage.
> Or what about you do an audio or video codec? Or an operating system?
There are already plenty of examples out there, Claude can probably help you there regarding history of such products not written in C, or where C required help from Assembly code.
You can start by researching IBM i, z/OS, OS 2200, Xerox Alto, DirectX and Metal (C++ for the most part, and Objective-C++ on the 2nd)
> This requires intrusively linked lists,....
And the C99 version is impossible to be written in Ada95 because?
Using C++ as your other comparison point when arguing that C isn't low level is by far the most smartass idea in this thread.
The only complaint I see about C++ not being capable is the correct observation that more platforms have C compilers than C++.
Either way, C++ spans a big range that goes just as low level as C. Even if these complaints are real they don't make it a reasonable comparison point for the "C is actually high level" argument.
This is common with C, when interfacing with hardware.
What exact C code did you had in mind?
So that the counter example is close enough to it in exposing the same semantics.
What are you even arguing right now? (Btw -ansi compiler flag)
> Smart-arse is comparing C versus JavaScript
I chose JavaScript to make the idea of a spectrum clearer using extremes. I can do C++ if you like. The machine doesn’t care about destructors, move, concepts, initializer lists, virtual methods, launder, or inheritance. You are programming against an abstract model further divorced from how x86 CPUs work.
To me choosing JavaScript as example against C, feels like the Tiger Beetle guy that initially chose JavaScript and then went to Zig because JavaScript did not deliver, go figure.
So many systems languages to chose from since 1958.
This one of the failures of Linus T. with the linux kernel: he was not able to keep the assembly source code with plain and simple C code you can compile with a small and alternative C compiler (same failure for the glibc devs I think).
I don't blame him, he is already keeping the linux ABI stable, and pulling that off is something.
Each additional compiler supported by a project means variance in functionality and thus additional work for the project. That work could make the codebase more robust. Or it could be a ton of useless work. Or anything in between. Depends on the context of the project.
Which part of that big clause is the part that failed? Because I thought you could still compile Linux with TCC.
With those assembly source files (which do not abuse any pre-processor) and plain and simple C, I could build a modern x86_64 linux kernel with cproc/qbe (which gets 70% of gcc speed in my CPU intensive benchmarks... for a few % of gcc code and in plain and simple C, not brain damaged c++).
But I kind of don't mind since the future is assembly coding on non-IP-locked standard like RISC-V, and the main issue for that future is the abuse of pre-processors (ffmpeg was bitten by it) or code generators which would not be written in assembly themselves (or with a simple high level language with an assembly written interpreter, asmpython?).
It was already outdated by the time Borland released Turbo C++ 1.0 for MS-DOS, and only got new wind thanks to GNU FOSS and their manifest to prefer C as the main compiled language for GNU projects.
Everywhere else outside UNIX, was going with a mix of C++ for OS frameworks, Apple, Microsoft, IBM, Be, Nokia, Epoch,....
Naturally given the option, between C, C++ and something else I might prefer that something else, however I managed a few interesting positions exactly due to my C++ skills, and interests.
So don't mix my preferences for C and C++ on the same basket.
> So don't mix my preferences for C and C++ on the same basket.
That mistake is mine indeed, I'll remember. Thanks, and I hope "no harm meant" was implicit :)
> and I hope "no harm meant" was implicit :)
Ad hominen then an apology, mixed signals here or I'm missing something. Maybe sarcasm?
I think many of us throughout the years been reading pjmlp's comments which fits a certain "theme". I don't mind though, it's just text after all, but was hard to keep myself from entering the meta-conversation when the opportunity just sat there. I still don't mean no harm by it, we all have our less agreeable ways of writing our comments, I'm surely guilty of it in some way too.
You can express annoyance at someone's pattern of behavior without it being personal. embedding-shape isn't the only person annoyed by pjmlp's repeated disdain and snark towards people who use C (or Zig or WebAssembly or Rust or...).
No, you're usually the initiator. Usually it's with some off-hand quip about how C programmers don't understand C, or how the people designing WebAssembly are ignorant of COM or the JVM, or how Zig is just Modula-2, etc.
Most threads you participate in aren't filled with snark until you enter them.
Two measures two weights, in C versus other systems languages.
I'm not pjmlp but I can explain this for the case of Rust, where this works a bit like C but with a few interesting differences.
Mainly, in Rust there is not a concept of a "memory object" per se in the runtime semantics. Memory is made of allocations and allocations are made of bytes. Unlike C, bytes are guaranteed to be 8 bits in size. Every byte of memory can hold integer values (0x00 to 0xff), pieces of a pointer or be uninitialized. That means there is nothing like strict aliasing, and therefore no need to have special rules for byte-level access. You can alias any type as any other type, so long as you avoid all the other sources of UB (out-of-bounds access, uninitialized memory access etc.).
The way to practically access this is much the same as in C. You can do things like cast pointers between different types and project a pointer to a struct to a pointer to one of its fields. It should be noted that, unlike with major C implementations, structs do not have a stable, well-defined layout, so if you do manual pointer math you need to put #[repr(C)] on the struct to get C layout rules (which might still yield platform-dependent field offsets, e.g. size_t is not the same size everywhere).
Note also that these are the dynamic rules of Rust, you need to follow these when writing unsafe code to avoid UB. The static rules of safe Rust are much more restrictive and don't allow much at all. It is possible to write unsafe code that exposes safe abstractions for this, one example is the "bytemuck" crate. It provides macros that can parse a type definition to check certain properties (e.g. well-defined layout, no padding) and then provide you with safe functions for byte-level access. Since there is no strict aliasing, for certain types you can also get safe functions for access at other granularities. For example:
#[repr(C)] struct Foo {
x: u32,
y: u16,
z: u16
}
can be safely accessed as an array of u32 values (uint32_t in C), but #[repr(C)] struct Bar {
x1: u16,
x2: u16,
y: u16,
z: u16
}
can not, for alignment reasons.BTW: If you use character-pointers, you also do not need to worry about strict-aliasing in C.
Or for something more modern either D or C++ will do.
Examples omitted on request.
Being able to find someone who's made the argument you're rebutting doesn't make it not a straw man. What matters is whether the person you're arguing with is making the argument.
Specifically this:
> When other languages have compiler specific extensions beyond the spec, it is a failure in their design.
Is not a point I've seen anyone here make.
Any code that ventures anywhere near that territory is 99% Undefined Behavior. It's almost impossible to write proper C/C++ code that isn't UB while touching byte-level representations.
This is undefined behavior!
const int* magic_intp = (const int*)bytes;
Heck even something trivial like this is UB:
bool bar(char ch) { return isxdigit(ch); }
The only safe thing to do is memcpy, but that's super useless. As soon as you try to interpret or manipulate the byte-level data in any way, there are UB traps everywhere you go.
Saying inline ASM is no different than a function call is like saying standard control structures are no different from function calls. I suppose from a Smalltalk perspective that could be true, but is that the mental model most programmers use?
I work on a system from the 90s with custom instructions. GNU-as was patched to understand the instructions. They’re used through macros that ultimately expand to inline ASM. Without this, you’d need function inlining, which may or may not be possible with a linked object (it certainly wasn’t standard in the 90s). So now a single instruction turns into stack management, a jump, more stack management and a return. At that point any benefit to a specialized instruction may be erased, or in the case I’m dealing with talking to external hardware becomes unreasonably expensive.
lets say I really want to use popcnt in my inner loop. with inline assembly I can just shove it in there. external linkage forces a function call overhead that can't be inlined, which obviates any benefit I might have had from using the specialized instruction.
its also true that when I unwrap my new spin with fancy new instructions its unlikely to have a robust set of instrinsics around them.
inline asm is a real mess, I always regret tussling with it, but its kind of pragmatically necessary if you're actually working at the metal in a high performance or embedded context unless you're doing the whole thing in assembly.
C exists in a nether world of being neither assembly nor high-level language.
People only call it high level because in the 1970s, having blocks, loops, and functions was high level, compared to the SoTa machines available in the day, which were either programmed with assembler or some bespoke thing the manufacturer came up with.
C only exists instead of the alternatives, because according to Dennis Ritchie himself it was more fun to create C than using something else, and I quote:
"Although we entertained occasional thoughts about implementing one of the major languages of the time like Fortran, PL/I, or Algol 68, such a project seemed hopelessly large for our resources: much simpler and smaller tools were called for. All these languages influenced our work, but it was more fun to do things on our own."
From https://www.nokia.com/bell-labs/about/dennis-m-ritchie/chist...
"All these languages influenced our work, but it was more fun to do things on our own"