Live data from Hacker News

Modern C (2019)

gustedt.gitlabpages.inria.fr

41–50 of 57 posts

Re: Modern C (2019)

#41
post #36

Earlier quoted context omitted.

> Now what happens, if we're trying to define behavior? The cell whose address is three billion greater, mod the size of the address space, now has a value of 20? > If the semantics aren't a direct translation into CPU opcodes, then it's not machine code. Just because the semantics are defined that way doesn't necessarily mean you couldn't make another implementation that obeys those semantics. For instance, QEMU can…

> The cell whose address is three billion greater, mod the size of the address space, now has a value of 20? Cool. It sounds like you figured out a restricted form of pointers for stack variables, since there's no danger of corrupting the code or breaking important invariants elsewhere. Now imaging tightening those restrictions a bit. Instead of wrapping pointers inside the entire stack, wrap/constrict pointers insid…

> Now imagine tightening those restrictions a bit.

Yep, that's one approach, though that's just as hard to compile as one that is defined to trap and end execution on an out-of-bounds write.

As I said up-thread, it's possible to define a C dialect without UB, it's just not what most of its users actually want out of the language, since on current hardware it adds significant runtime overhead.

> The machine [...] the compiler

Isn't QEMU both the machine and the compiler (EDIT: perhaps better-put, what's the distinction? They're both just the implementation.) The bytes only need to be unchanged on read from inside the program; if you compile them to something else, that's fine. Prolog programs can introspect with clause/2 and it doesn't cause trouble there.

Re: Modern C (2019)

#42
post #40
post #4

Earlier quoted context omitted.

C biggest "mistake" (there are historical reasons for this, though, but it doesn't make it acceptable nowadays) is the concept of undefined behavior, period.

No. The mistake is conflating "undefined behavior" with " unspecified behavior". Signed overflow was not intended to be undefined--it was unspecified because it has a perfectly acceptable specification that differs between processors, and it was expected that the compiler would define it . The fact that gcc and clang pick non-sensical specifications in order to gain 3% in performance was not something anybody expecte…

> Signed overflow was not intended to be undefined--it was unspecified

This seems wrong to me. The Standard very much calls signed overflow undefined behavior (Section 3.3):

> If an exception occurs during the evaluation of an expression (that is, if the result is not mathematically defined or not representable), the behavior is undefined.

In addition, overflow is explicitly listed as undefined behavior in at least two other places. For example, in section 1.6, Definitions of Terms:

> An example of undefined behavior is the behavior on integer overflow.

And in the list of undefined behavior (A.6.2):

> An arithmetic operation is invalid (such as division or modulus by 0) or produces a result that cannot be represented in the space provided (such as overflow or underflow) (3.3).

If signed overflow was not intended to be undefined, then it seems like a rather large oversight to say it's undefined behavior and to give it as an example of such.

> because it has a perfectly acceptable specification that differs between processors, and it was expected that the compiler would define it.

What you're describing is implementation-defined behavior in the C standard. If it was expected that implementations would define the behavior, why didn't the committee just... use that definition, instead of saying signed overflow is UB but actually meaning it's implementation-defined behavior?

In addition, unspecified behavior is a third category of behavior distinct from undefined and implementation-defined behavior. The Standard imposes no requirements on unspecified behavior in a correct program, and does not require that an implementation document what it does in such a situation.

[0]: https://port70.net/~nsz/c/c89/c89-draft.html

Re: Modern C (2019)

#43
post #41

Earlier quoted context omitted.

> The cell whose address is three billion greater, mod the size of the address space, now has a value of 20? Cool. It sounds like you figured out a restricted form of pointers for stack variables, since there's no danger of corrupting the code or breaking important invariants elsewhere. Now imaging tightening those restrictions a bit. Instead of wrapping pointers inside the entire stack, wrap/constrict pointers insid…

> Now imagine tightening those restrictions a bit. Yep, that's one approach, though that's just as hard to compile as one that is defined to trap and end execution on an out-of-bounds write. As I said up-thread, it's possible to define a C dialect without UB, it's just not what most of its users actually want out of the language, since on current hardware it adds significant runtime overhead. > The machine [...] the…

> Yep, that's one approach, though that's just as hard to compile as one that is defined to trap and end execution on an out-of-bounds write.

> As I said up-thread, it's possible to define a C dialect without UB, it's just not what most of its users actually want out of the language, since on current hardware it adds significant runtime overhead.

So my core point is this: It's not UB that enables this optimization. When you ask "how can that optimization be legal without UB?", the hard part is "without UB" all by itself. If you have a language with UB, the optimization is easy to enable. If you have a language without UB, the optimization is easy to enable. That optimization is not an example of why we need UB.

There can be significant runtime overhead to remove UB, but it's not in service of enabling that optimization.

> Isn't QEMU both the machine and the compiler (EDIT: perhaps better-put, what's the distinction? They're both just the implementation.) The bytes only need to be unchanged on read from inside the program; if you compile them to something else, that's fine. Prolog programs can introspect with clause/2 and it doesn't cause trouble there.

Basically, I don't think "The bytes only need to be unchanged on read from inside the program" is true. If you're compiling machine code you're not in charge of the entire computer. The code might depend on other code looking at the bytes, and you won't be able to intercept that unless you do some kind of ridiculous rootkit takeover when the compiled program launches, creating a virtual machine and moving everything that was already running into it. And I don't just mean that in a theoretical gotcha sense, real libraries sometimes need to alter function calls in other libraries.

Re: Modern C (2019)

#44
post #27

Earlier quoted context omitted.

I'm okay with undefined behavior, but the language should revert back to the original (an exhaustive list of permissible behavior by the host when the C abstract machine's behavior is undefined) rather than the current mere list of examples of possible behavior by the host.

"I'm okay with undefined behavior, as long as its behavior is well-defined." You keep using that word. I don't think it means what you think it means. :)

Plenty of things are undefined, yet bounded. And being bounded does not mean that something is well-defined.

The number of elements of the set of real numbers between 1 and 2 is undefined, but we can say with absolute certainty that 3.5 isn't one of them.

Re: Modern C (2019)

#45

I enjoy programming in C a lot, though I wish they would fix C's biggest mistake[1]. [1] https://digitalmars.com/articles/C-biggest-mistake.html >

I don't entirely agree with this article ... it's quite possible to "pass" an array as a pointer to an array type, as opposed to a decayed pointer, e.g.

  void f( int (*a) [5] );
  int a[5] = {1, 2, 3, 4, 5};
  f( &a );
The larger problem here is more that this mechanism doesn't support arbitrary sizes. Which probably makes sense from how c handles memory, but it's very restrictive.

Incidentally, I recently had an argument with a friend who had been "coding for years" in c, that the memory address of "a" and "&a" in the example above are identical. He thought it was so obvious I was wrong (because pointers) that I had to code it and print the addresses for him and prove they're the same. He was completely mind-blown xD

Re: Modern C (2019)

#46
post #4

I enjoy programming in C a lot, though I wish they would fix C's biggest mistake[1]. [1] https://digitalmars.com/articles/C-biggest-mistake.html >

C biggest "mistake" (there are historical reasons for this, though, but it doesn't make it acceptable nowadays) is the concept of undefined behavior, period.

UB is not a bug, it's a feature.

Re: Modern C (2019)

#47
post #11
post #5

Earlier quoted context omitted.

The declaration syntax and the lack of a proper module/namespacing system is also a mistake, but perhaps not as big as the way arrays decays into pointers. Implicit conversions I suppose could also be classified as a mistake.

What you're calling "mistakes" look to me like intentional design decisions made in order to optimize for specific kinds of problems. C is not a high level language, nor should it be. If you're doing the sort of work that is better suited for a high level language, then using a different language is the right call.

Well, IIRC even in the K&R book, a book which many C programmers admire for it's excellent documentation and prose, and rightly so, there is a specific section mentioning that C's declaration syntax has been castigated.

Next, even if C is not a "high level language", which depend on your definition of "high level language", the perks of a proper module system for low level coding cannot go unmentioned.

- Rust, a language that lives in a somewhat similar abstraction and power space as C, has a module system.

- C++, a language that lives closer to C in features, also now, in C++23 I believe, has a module system, and before that, had namespaces.

On implicit conversions, I think having implicit conversions are hard to get right, and I think that C has them is not a feature, but rather a bug. Languages that have taken some inspirations from C, has chosen usually to do away with implicit conversions, or at the very least, limit their use.

And the array -> pointer in functions, I percieve to be a big mistake, not at the time, perhaps it was not known better ways, but as the years roll by, I think it's a misfeature given the countless exploits that have taken place due to a read past the bounds of an array.

I think C is a good language, but these misfeatures burdens the language in an undue way, and I think a better way to design a language is to be more explicit. Something like Rust's

    unsafe { /* Here be dragons */ }
is better design, in my opinion. I think if you want implicit conversions, there should be an explicit way to declare that you want things to be implicit.

That's just my 2¢

Re: Modern C (2019)

#48
post #41

Earlier quoted context omitted.

> The cell whose address is three billion greater, mod the size of the address space, now has a value of 20? Cool. It sounds like you figured out a restricted form of pointers for stack variables, since there's no danger of corrupting the code or breaking important invariants elsewhere. Now imaging tightening those restrictions a bit. Instead of wrapping pointers inside the entire stack, wrap/constrict pointers insid…

> Now imagine tightening those restrictions a bit. Yep, that's one approach, though that's just as hard to compile as one that is defined to trap and end execution on an out-of-bounds write. As I said up-thread, it's possible to define a C dialect without UB, it's just not what most of its users actually want out of the language, since on current hardware it adds significant runtime overhead. > The machine [...] the…

> that's just as hard to compile as one that is defined to trap and end execution on an out-of-bounds write

I believe simply disallowing "+" and "-" from accepting pointer types is slighlty less work than writing logic for "integral + pointer", "pointer + integral", "pointer - integral", and "pointer - pointer". Although on the other hand it means that instead of literally rewriting "a[i]" to "*(a+i)" in the AST, one has to write a separate logic for array accesses so... okay, so it's exactly as easy to compile ("as hard" technically works as well, but since it's actually rather easy, not hard, I've chosen "as easy", hope you don't mind).

Re: Modern C (2019)

#49

I enjoy programming in C a lot, though I wish they would fix C's biggest mistake[1]. [1] https://digitalmars.com/articles/C-biggest-mistake.html >

I don't entirely agree with this article ... it's quite possible to "pass" an array as a pointer to an array type, as opposed to a decayed pointer, e.g. void f( int (*a) [5] ); int a[5] = {1, 2, 3, 4, 5}; f( &a ); The larger problem here is more that this mechanism doesn't support arbitrary sizes. Which probably makes sense from how c handles memory, but it's very restrictive. Incidentally, I recently had an argument…

You can’t pass an array by value and you can’t pass a pointer with a corresponding length of non-fixed size (you can actually with VLAs, but frankly the syntax sucks and not all compilers support it). This missing language feature has led to horrible things like nil-terminated strings , a million bespoke ways to encode the length of the array, or just functions that pray you have enough space like `strcpy` and `gets`.

Re: Modern C (2019)

#50

Earlier quoted context omitted.

Every language has undefined behavior - it's just that some compilers emit an error when it's encountered. You can make a C compiler that refuses to compile code with undefined behavior.

Most undefined behaviour can’t be reliably detected at compile time, though you could throw the error at runtime or make the behaviour defined instead

...which is what we're using ubsan for.
Post reply on HN