117 karma · joined April 13, 2020
I'd also say there is consensus that (2) would be beneficial. There are some good ideas in http://www.open-std.org/jtc1/sc22/wg14/www/docs/n2067.pdf although I don't think repurposing the register keyword for it was very popular. Not just because it wouldn't be compatible with C++ which deprecated register some time ago, but also because it's novel with no implementation or user experience behind it. My impression that this is waiting for a new proposal.
What might perhaps be more challenging is adjusting to the changes in compilers. They tend to optimize code more aggressively and so writing code that closely follows the rules of the language (rather than making assumptions about the underlying hardware, even valid ones) is more important today than it was back in the 80's.
The effective type of an object for an access to its stored value is the declared type of the object, if any. If a value is stored into an object having no declared type through an lvalue having a type that is not a character type, then the type of the lvalue becomes the effective type of the object for that access and for subsequent accesses that do not modify the stored value. If a value is copied into an object having no declared type using memcpy or memmove, or is copied as an array of character type, then the effective type of the modified object for that access and for subsequent accesses that do not modify the value is the effective type of the object from which the value is opied, if it has one. For all other accesses to an object having no declared type, the effective type of the object is simply the type of the lvalue used for the access.
int a[2][2];
int f (int i, int j)
{
int t = a[1][j];
a[0][i] = 0; // cannot change a[1]
return a[1][j] - t; // can be folded to zero
}
The language says that elements of the matrix a must only be accessed by indices that are valid for each bound, so compilers can and some do optimize code based on that requirement (see https://godbolt.org/z/spSF8e).But when a program breaks that requirement (say, by calling f(2, 0)) the function will likely return an unexpected value.
But we must write code that relies on the same rules and requirements that programs are held to (and vice versa). When either party breaks those rules, either accidentally or deliberately, bad things happen.
What sometimes happens is that code written years or decades ago relies on the absence of an explicit guarantee in the language suddenly stops working because a compiler change depends on the assumption that code doesn't rely on the absence of the guarantee. That can happen as a result of improving optimizations, which is often but not not necessarily always motivated by improving the efficiency of programs. Better analysis can also help find bugs in code or avoid issuing warnings for safe code.
There's some debate about the effects of reading an uninitialized local variable of unsigned char (like whether the same value must be read each time, or whether it's okay for each read to yield a different value).
This special exemption doesn't extend to any other types, regardless of whether or not they have padding bits or trap representations that could cause the read to trap. Few types do, yet the behavior of uninitialized reads in existing implementations is demonstrably undefined (inconsistent or contradictory to invariants expressed in the code of a test case), so any subtleties one might derive from the text of the standard must be viewed in that light.
I/O functions that take a plain char* are designed to interoperate with char arrays and strings, so passing in unsigned or signed char is a sign that they aren't being used as intended. (Functions that traffic in binary data like fread/fwrite should take void*).
But making function signatures const-correct solves only a small part of the problem. A new API can only be used in new code, and casts can remove the constness from pointers leaving open the possibility that poorly written code will inadvertently change the const object. An attempt to change a global variable declared const will in all likelihood crash, but changing a local const can cause much more subtle bugs.
In my view, a more complete solution must include improving the detection of these types bugs in compilers and other static and even dynamic analyzers even without requiring code changes. It's not any more difficult to do that detecting out of bounds accesses. (In full generality it cannot be done just by relying on const; some other annotation is necessary to specify that a function that takes a const pointer doesn't cast the constness away and modify the object regardless.)
Adding a rule requiring implementations to error out in cases of undefined behavior would be hard to specify in the standard. It could (and in my view should) be done by providing non-normative encouragement as "Recommended Practice."
There are efforts to define the behavior in cases where implementations have converged or died out (e.g., twos complement, shifting into the sign bit).
There have been no proposals to add new array types and it doesn't seem likely at the core language level. C's charter is to standardize existing practice (as opposed to invent new features), and no such feature has emerged in practice. Same for modules. (C++ takes a very different approach.)
It might seem like defining the semantics for signed overflow would be helpful but it turns out it's not, either from a security view or for efficiency. In general, defining the behavior in cases that commonly harbor bugs is not necessarily a good way to fix them.
Reviewing proposals to incorporate features supported by common implementations is another.
Aligning with other standards (e.g., floating point) and improving compatibility with others (C++) is yet another.
In general, when an ISO standard is done it essentially becomes dead. So for the C standard to continue to be active (on ISO's books) it needs to evolve.