That's hardly any better than undefined behavior. It's not the program you want to write, under any possible circumstance, and the language should tell you so.
It is significantly better than undefined behaviour, as it results in a noisy failure rather than silently incorrect results. Compile-time failures are best, sure, but I'll always take an exception over what is essentially data corruption.
> It is significantly better than undefined behaviour, as it results in a noisy failure rather than silently incorrect results.
This might be true in a deterministic setting, since the likelihood that a test suite will find the error is very high. But in a non-deterministic concurrent setting, throwing an exception that might be only caught once in a blue moon is just as bad as not doing anything about errors.
An occasional exception and silent corruption the rest of the time is still a lot better than silent corruption all the time - it's much easier to notice in the first place, and as long as you have one instance of the exception you have a stack trace to start from. And in practice the JVM/standard library throws ConcurrentModificationExecption pretty reliably.
The stack trace tells you where one of the concurrent accesses happened. As opposed to data corruption where (if you even manage to witness it in the first place) all you know is that one of the things that accesses that memory got it wrong.
You can look at the code, see why you can't prove it correct, or work backward to see how the code could have gotten to such a state. Like I said, debugging skills.
Of course I can look at the code. Trust me the reason why I introduced the bug wasn't because I was looking at something else at the moment.
> see why you can't prove it correct,
Realistically, this is because the language and the program's design conspire to make proving anything about the program an uphill battle. If the language could perform basic sanity checks (e.g., no attempting to use objects after ownership has been transferred to someone else), then at least I could have a fighting change to manually prove more interesting properties.
> or work backward to see how the code could have gotten to such a state.
Doing this on a per case basis is an incredibly mind-numbing task.
Pedantically, it's defined behavior, and doesn't use an interpreter lock.
Granted, I'm completely head over heels for Rust, and I agree completely that ConcurrentModificationException is a crappy answer, but it is defined behavior (AFAIK).
It's not defined. The library/VM is very good at throwing ConcurrentModificationException in practice, but it's specified as a best-effort thing, not to be relied upon.
It's true that throwing exceptions is defined behavior, but it's not defined in a way you'd actually want to use. For your actual purposes, it's undefined.
??? Ask any C programmer if they could wave their magic wand and turn every single undefined behavior in their programs into a segfault how much better their life would be. It's not a small improvement it's a huge improvement.
> UB is simply the latest stick to hit C with. In day-to-day working nobody worries about UB at all as you generally don't notice it.
I agree that in day-to-day working nobody worries about it, and I see odd behaviors all the time because of it. In particular as fewer applications are using C, a much higher fraction of C becomes systems and embedded where you are more likely to accidentally run afoul of choices that were made to compete with FORTRAN on numerical performance.
A read from NULL will crash on most unixen, but will not crash on some targets without an MPU and when running in kernel mode, so may be left lurking (see the linux kernel).
The C89 aliasing rules in particular are completely at odds with a lot of kernel and device driver code, and in addition where the int size was 2 bytes previously but is now 4 bytes you can have signed overflow where before the behavior was well defined:
UINT2 x; // 16 bit integer
...
x+=2; // addition mod 2**16 on 16 bit targets, undefined behavior on 32 bit targets.
These are some real-world bugs I've dealt with.
> (and yes, I do have plenty of experience in it, I've been using it for the past 20 years, have you?).
I've been using it professionally for only about 15 years, but I started using C at home in '92.
[edit]
> Same with lack of a GC; this is a plus point for C for most applications, not a negative.
This is a bit of a non-sequitur, as I didn't mention memory management at all. C doesn't need a GC. It could use more memory safety though. There's been plenty of academic research on improving C's memory safety without significant runtime overhead; a lot of those techniques were used in rust. There are plenty of tools that can catch a large fraction of memory errors at compile time, which is a good thing.
The vast majority of all code you write will not invoke UB, most people tend to stick to an 'easy' subset of syntax, unlike say C++ where everyone uses a different subset of features making it in effect multiple languages.
A combination of testing the known edge cases, wraparound issues, size issues, static analysis and tooling means running into an example of UB is extremely rare in most cases.
It used to be that people used dynamic memory allocation to beat C with, but that is just a resource management issue. TBH, this is not rocket science. If you need dynamic memory allocation, you had damn well better know how to use it properly.
Its an example of laziness and people ignoring the machine.
Another example is performance; saying that a language comes within a factor of 2 of C's performance and therefore is fast is absolutely ridiculous. a factor of 2 is huge.
You have to remember that people who write C are dealing with machine-specifics day-in, day-out. we're bit-fiddling and writing MPU code and drivers, etc.
Basically, we're much more aware of the machine than higher-level softies, so what would normally be UB is actually DB in most cases, its defined by the compiler and hardware that we're intimately familiar with.
...and that isn't to say that you can't write high level abstracted code in C, the simplicity of the language lends itself to (properly) efficient implementation, not efficient in the sense of Java or Ruby ;o)
> The vast majority of all code you write will not invoke UB
That's a bit like saying “the vast majority of the haystack doesn't contain any needles”.
> most people tend to stick to an 'easy' subset of syntax
I'm not sure I understand what you mean. Undefined behavior has nothing to do with syntax. It's strictly a semantic issue.
> It used to be that people used dynamic memory allocation to beat C with, but that is just a resource management issue. TBH, this is not rocket science.
If I understand correctly, the objection isn't that it's rocket science, but rather that you get little help from your tools if you do it wrong. Memory debuggers will only tell you about memory management bugs that manifest themselves in a particular program run. If a bug will only manifest itself under conditions that are hard to replicate, you're out of luck.
Of course, none of this is an indictment of manual memory management per se, or suggests that garbage collection is a universally good solution. But manual memory management has usability issues, which fortunately being addressed in more modern language designs like Rust.
> If you need dynamic memory allocation, you had damn well better know how to use it properly.
Sure, but are better compile-time diagnostics too much to ask for? Notice that compile time diagnostics don't introduce any runtime performance penalty.
> Another example is performance; saying that a language comes within a factor of 2 of C's performance and therefore is fast is absolutely ridiculous. a factor of 2 is huge.
No disagreement here.
> so what would normally be UB is actually DB in most cases,
As far as I can tell, the trend among C and C++ compiler writers is to optimize programs very aggressively under the assumption that UB simply will never happen, rather than to turn UB into DB.
> its defined by the compiler and hardware that we're intimately familiar with.
Well, “works on this machine” isn't good enough for most of us.
You're correct.... the vast majority of the haystack doesn't contain any needles, thats the point. And you also know where the needles tend to be and stay away from that area.
I'm not implying that UB doesn't exist, simply saying that using C is a different mindset.
If you use C, you dont just use the language, you use the language, the toolchain and the machine, you're familiar with the whole stack, quite often down to the metal.
The point about memory management is that memory management is just case of the general problem, i.e. resource management. resource management is a skill you need to have if you're a softie and making it easier in one specific case (RAM) is not a generic solution. Better that you learn how to do it properly then apply that knowledge in all situations (files, RAM, power, etc).
e.g. where is the GC-equivalent for power management? or file handles? its the same problem in a different domain.
> The point about memory management is that memory management is just case of the general problem, i.e. resource management.
Wholeheartedly agree. I'm aware that GC is no solution for this problem. But I'm not arguing in favor of GC - I'm arguing in favor or making manual resource management safer, for example, like Rust does. Resource management is every single bit as manual as in C - the only difference is that the compiler yells if you do it wrong.
> The vast majority of all code you write will not invoke UB, most people tend to stick to an 'easy' subset of syntax, unlike say C++ where everyone uses a different subset of features making it in effect multiple languages.
Even integer addition very easily leads to undefined behaviour.
> It used to be that people used dynamic memory allocation to beat C with, but that is just a resource management issue. TBH, this is not rocket science. If you need dynamic memory allocation, you had damn well better know how to use it properly.
If you're going to solve a quadratic equation you should damn well know how to do it properly, by completing the square. But once you know that you should use the formula, because it makes it a lot easier. If you complete the square every time out of pride, you're just wasting everyone's time.
> A combination of testing the known edge cases, wraparound issues, size issues, static analysis and tooling means running into an example of UB is extremely rare in most cases.
Sure. You can do enough work to eliminate it. Or you can use a language where you don't need to.
> Its an example of laziness and people ignoring the machine.
Laziness is one of the cardinal virtues of a programmer
> Another example is performance; saying that a language comes within a factor of 2 of C's performance and therefore is fast is absolutely ridiculous. a factor of 2 is huge.
A factor of 2 is irrelevant most of the time. If you're growing exponentially, a factor of 2 will let you put off the point where you have to start scaling out by maybe a few months. If you're not growing exponentially, you probably won't hit performance limits at all.
> Basically, we're much more aware of the machine than higher-level softies, so what would normally be UB is actually DB in most cases, its defined by the compiler and hardware that we're intimately familiar with.
Until the compiler adds new optimizations. Sure, if you're never going to upgrade the compiler maybe you can get away with C.
> ...and that isn't to say that you can't write high level abstracted code in C, the simplicity of the language lends itself to (properly) efficient implementation, not efficient in the sense of Java or Ruby ;o)
Without native tagged unions you won't get far up the abstraction ladder. You can write your own with macros sure, but they won't interoperate with anyone else's or any libraries you'd want to use.