But being a cynic, in the past when I see good guidelines posted, it is not long before all the exact opposite happens and the guidelines are meaningless :(
It just has to be known that tripping those guardrails is undefined.
Even assuming you meant undefined behavior instead of unspecified behavior, it's still not a particularly practical prospect, for you basically prohibit any use of pointers (or memory access in general) whatsoever.
There are other places where we can prove that code is safe in most conditions but proving the exception never happens is equivalent to proving the halting problem.
Sanitizers exist and do detect a subset of UB via runtime checks, but there is nothing resembling a system that can detect all UB. That would basically require an entire VM implementing the C-standard with a truly outrageous amount of runtime overhead.
Strictly speaking, undefined behavior only means that the Standard does not impose a requirement.
But what many people mean is that the ramifications are outside the scope of the language or are unpredictable.
Writing through an invalid pointer is perfectly definable. The standard could easily specify solely that the write to the memory location indicated in the pointer will be attempted, but that whether the write succeeds and what second- and third-order effects result from such a write are dependent on the environment, including operating system.
Boom. Not undefined behavior. But still potentially dangerous behavior with unpredictable results.
(In my view, this is in fact what ANSI C requires for this type of "undefined behavior," but many others do not agree.)
By not imposing any requirements for undefined behavior, ISO C does not require that, but maybe it ought to.
> Strictly speaking, undefined behavior only means that the Standard does not impose a requirement.
> But what many people mean is that the ramifications are outside the scope of the language or are unpredictable.
The latter is a consequence of the former. Compiler writers treat undefined behavior as though it can't happen, and this causes unpredictable behavior.
Suppose you had code like this in C:
#define Variant1 0
#define Variant2 1
void use_variant(int variant) {
if (variant == 0) {
// Variant1
} else {
// Must be Variant2
}
}
You are assuming your code is always passed a 1 or a 0, not checking for any other possibilities, which is basically treating other possibilities as "can't happen". Code that passes it a 2 might get one kind of behavior now, and another kind of behavior later, when the function updates. Your function may have wildly inconsistent behavior on different versions of the same function when given inputs of 2 because you're treating that as undefined behavior. That's basically how compiler writers treat undefined behavior - just assume it doesn't happen.And, that's how standard C library functions work. Functions like strlen assume you never give it a null pointer; they will just dereference pointers unconditionally.
Yes, but this phrase is ambiguous. What it is meant to refer to is "ignoring the situation completely with unpredictable results," i.e., ignore that writing to that random address is theoretically unpredictable, and just do it, consequences be damned.
What compiler writers sometimes actually do is prove by static analysis that the pointer was freed, or never initialized, and then predicated on that situation, rather than ignoring that situation, generate machine code that elides it entirely. Since they know the code is undefined behavior, and they "assume UB doesn't exist," they assume the code cannot execute.
When strlen() keeps iterating through memory, the behavior is closer to the former than the latter.
This is simplified somewhat, since there are now flags to warn on potential null dereferences and to never delete any null pointer checks.
It sounds malicious, but it's simply oblivious. The same mechanism would delete null pointer checks in cases like these:
while (p && p->data != NUMBER) {
...
if (p && p->data < NUMBER) {
...
}
...
}
To enter the loop, the pointer can't be null, so that check can be rewritten to this: if (p->data < NUMBER) { ... }
And it works for other things, too. while (a && b && c)
if (a) {
...
} else if (b) {
...
} else if (c) {
...
}
Because the only way to enter the loop is for a and b and c to be true, that can be rewritten to the equivalent of this to remove unnecessary double checks: for (;;) {
if (a) {
...
} else if (b) {
...
} else if (c)
...
} else break;
}
Edit:Except, it doesn't rewrite source code. By the time it makes these optimizations, it's working with control flow graph in SSA form. These are all optimizations made after tons of passes, and it's harder to guess what the programmer intended. Some undefined behavior may be introduced by one pass and then gone after the next.
That's why there are both warnings about null pointer dereferences, and then it goes on to apply aggressive optimizations based on those null pointer dereferences anyway. The left hand doesn't know what the right hand is doing.
It could. But now you can't optimize things to store data in registers hardly ever.
If you want a language that does these compile-time and runtime checks on pointers and integers, but is otherwise like C (in that it compiles natively and doesn't use garbage collection), your choices are basically Rust and Ada.
C++ is actively working on profiles, and if you enable the right profile many of the easy ways to be insecure are not longer possible. Or at least that is the goal only time will tell if they make it in, and then more time if they are both used and make a difference. However C++ developers have long said "No naked new/delete", data structures where the invariant (length...) is enforced - thus a profile can remove new/delete and the places where we now know the data structure invariant isn't enforced while still being a usable language.
Because of the above anyone who is in C++ objects to phrases like "C and C++ or C/C++" - C++ is trying to leave C behind expect where compatibility with C is required.
What was it Dua Lipa sings? "If you're under him, you ain't getting over him".
C is foundational to C++. Look at your miserable type system. Do you think anybody who designs type systems built that? No. This is the C type system, it's defective but it was easy to implement.
Actually fixing C++ needs a ground-up rebuild, that's what you see (impressively) in Sean Baxter's work for example. I doubt it will make a real difference, but to his credit Sean looked at all the horrible problems and rolled up his sleeves and fixed them with a fairly radical set of required language and library changes, rather than kicking them under a rug and insisting it's all fine, just don't look under the rug.
I disagree with your conclusion, though. To me, the above means that C FAILS to meet the goal of enabling secure programming. An analogy might be car safety. Yes, if we had perfect drivers we would not even need seatbelts, let alone airbags. But, I would definitely say that a car without these does not meet the goal of "enabling" safe driving merely because it is theoretically possible.
https://crashstats.nhtsa.dot.gov/Api/Public/ViewPublication/...
Not to say that improved tools isn't also a worthy goal, but why not attack the problem from all angles?
And let's face it, given its bare metal nature, there are always going to be limits on how "secure" you can make it in order to "protect" the developer from goofing up in C/C++.