IME I've rarely, if ever, needed to make any of those assumptions, though early in my career I unfortunately did. Unless you're writing a kernel or a compiler there's rarely a good reason to assume a flat memory model. You would assume a flat memory model if you're manipulating objects in a way that grossly subverts the typing system; I say grossly because assuming a flat memory model usually isn't necessary even for most illegal type punning hacks.
Assuming 8-bit chars is useful, yes, but really only so you don't need to insert masks everywhere when doing bitwise operations on chars, such as when marshaling integer types (preferably without type punning). AFAIU an 8-bit ASCII string won't be packed 2 characters to a char on implementations using a 16-bit char, so pointer arithmetic and string manipulation will always look the same assuming the same string encoding. As with integers more generally, most of the time all that matters is that an integer type has at least N bits, whether or not you'll use them.
If you're depending on fixed-width types then usually there's a leak in your abstraction somewhere or you're doing bitwise manipulations where it's probably wise to use explicit masking, if only to make the code more clear, if not provide the ability to parameterize the value range. Typical exceptions would be cryptographic code, hashes, etc, particularly code that needs to perform rotations. But thankfully C has provided fixed-width types for nearly 20 years now, and almost all (if not all) compilers, even niche compilers, support these.
Assuming two's complement is only useful when you're depending on overflow characteristics. But signed overflow is undefined, anyhow (and for still-good reasons), and unsigned integer types are already guaranteed to be using two's complement representation (which requires emulation on some architectures, similar to how compilers transparently synthesize 64-bit long long integers on 32-bit platforms).
I've never done heavy floating point arithmetic so can't speak to the usefulness of assuming IEEE floating point for, e.g., managing error accumulation. But IME general purpose software likes to assume IEEE floating point for its indirect benefit, like how JavaScript provides the ability to perform accurate 32-bit scalar arithmetic by dint of providing a single 64-bit IEEE floating point integer type, or how VMs (including JavaScript JIT VMs) abuse floating point to implement tagged types; but arguably the reliance on 64-bit IEEE floating point has been on balance detrimental by, e.g., making the adoption of 64-bit scalar arithmetic in languages like JavaScript more difficult.
Also, it's noteworthy that WebAssembly makes use of some of the flexibility (or, rather, stricture) of the C standard. Appreciating the subtleties of C semantics helps make your code portable not only to archaic environments, but to bleeding-edge and future environments.
And yes, you can do that. The question is, why have all this complexity when in practice int32_t is there pretty much all the time - as you've said yourself - and when lots of existing code relies on it?
Same thing on other counts. Yes, you can work without all these assumptions. But why waste time on making the effort, when it simply doesn't matter?
For C2x and C++2x it looks likely that they will drop support for sign-magnitude and ones' complement representations of signed integers, essentially mandating two's complement.
https://gustedt.wordpress.com/2018/11/12/c2x/
https://herbsutter.com/2018/11/13/trip-report-fall-iso-c-sta...
http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2018/p090...
No and why should they? I assume all these things routinely when I write code that I don't expect to run on "weird" systems. And I don't have to worry about int being less than 32 since I when I need this guarantee use "int32_t" like a civilized human being.
The overhead is only here if you care to really write entirely portable C code but in practice very few people bother to do that outside of comp.lang.c.
>It would be nice to have an ISO-standard superset of C that catalogs such assumptions. Basically, a "non-DSP, non-mainframe" version of C, that's portable across all modern non-exotic platforms, and provides definitions for as many things that are UB or implementation-defined in standard C as possible.
POSIX C then? It assumes that chars are 8bits, that data and function pointers are castable and a bunch of other assumptions that are not possible in strict C. It also offers a richer API than C's stdlib. When I write C for a "non-DSP, non-mainframe" environment (that is, 99.9% of the time) I almost always target POSIX C, not strict ANSI.
And yes, POSIX C is a very good example (and I completely forgot about the function/data pointer thing... that one is probably one of the most common assumptions people rely in, and I bet most of them don't even know that it's not standard ISO C!). It's a great starting point, but POSIX being as old as it is, it doesn't include some things - e.g. it doesn't guarantee any particular size for int above and beyond ISO. At the same time, the richer POSIX API is not actually available on some platforms - notably, Win32 - so taken as a whole, it's not really a pragmatic portable superset, if you want to target all mainstream platforms.
What I was thinking about is more along the lines of, look at existing platforms that "matter", find the set of common assumptions that ISO C doesn't guarantee, but which all the platforms nevertheless provide in practice, and give an official blessing to that set.
I feel like if you start going down that road, towards “commonly portable”, you naturally end up with one of the not-c variants (zig, nim, etc). ...and ifc, if you start writing a new language, you might as well toss in features from the last thirty years of language design.
C is C largely because its so stupidly (and impressively) portable; if you aren’t drawing that benefit, its likely a case of the wrong tool for the job.
I'm working in medium sized embedded systems (not embedded Linux, but also not the smallest embedded systems). I've only rarerly worked on 8 or even 16bit Microcontrollers. Most of what I work on is 8bit chars, 32bit, Little Endian normal Microcontrollers.
I really don't need the portability, the targets are all the same, however they only have C/C++ compilers. And we can't use GC. Rust would be a very nice choice at least for the application code. I'm not sure how to write low level drivers in Rust, but with FFI I would just push that to the boundaries, and make at least 90% of the applications Rust. But there's no LLVM backend for most embedded processors I work with, and even if there is, the maturity might not be there, and no company would switch to that considering that the next target might not have a backend.
We have to use C, because it's the only thing that we are offered. But for desktop applications I see no real reason to start something in C nowadays.
https://www.mikroe.com/compilers
But as you say, the company needs to give the option to developers.
But you can already make those assumptions, and safely ensure that they are satisfied by using compile-time asserts (i.e. static_assert), which are very powerful in C++.
Need to know that int is 32 bit? Not a problem. Need to know that long is strictly larger than int? Not a problem. And if you do break someone's build with these static_assert's, that's fine -- it wasn't intended to be a supported platform anyway.
The other problem with this approach is that it effectively defines lots of distinct subtly different pseudo-platforms, each a set of assumptions. And your average developer might not know which of those assumptions actually are "portable enough", and which are not. The point of defining such a profile would be for experts to provide a blessed set of assumptions that is, in fact, portable across all general-purpose platforms that are in active use.
For example, the fact that CHAR_BIT is 8, or that int32_t is defined, or that ints are two's complement, are all reasonable assumptions. But two's complement overflow for signed arithmetic is not, even today - even though in practice it works just fine on some platforms (more specifically, on some implementations).
I'll continue under the assumption that C is capable of equally powerful compile-time asserts.
> it effectively defines lots of distinct subtly different pseudo-platforms, each a set of assumptions
Indeed, but using this approach, all that's needed to define a new 'ordinary platform C' is a clearly defined set of requirements. No need for a whole new standard in the usual sense.
> experts to provide a blessed set of assumptions
Sure, and that's compatible with this approach. It could take the form of a header file.
Good question, I'm not certain that's possible. A quick google turned up surprisingly little.
The ghastly hack alternative would be to detect which compiler/target we're dealing with and compare against a whitelist.
> I think this sort of thing would need some official blessing to be widely adopted, regardless of whether it's a written spec or a header.
It would certainly help to have it properly branded and given real credibility, yes. I'm reminded of MISRA C, which has a good deal of tooling and documentation.
I have 15 years of programming on 8but micros where that assumption/declaration is crazy. :)
Never use int. Always uint32_t. There! Problem of portability solved.
Right, but that's the point here - we're discussing how to describe a stricter variant of C, which make exactly the kinds of guarantees that preclude exotic architecture targets. Ultra-portability isn't a goal for many codebases.
By 'standard' do you just mean more widespread use? Some APIs already do something like this, such as Windows with its 'DWORD'.
It's not that the ability to write code that is so portable isn't useful. For something like a library of algorithms (e.g. compression - think zlib), there's actual value to be derived from having a single ultra-portable implementation that can run everywhere. But does something like Evolution or LibreOffice really need to never assume that CHAR_BIT is not necessary 8, or that int might be less than 32, or that int64_t might not even be defined? I would say that of all the C and C++ code that's running on modern devices today, the vast majority could safely assume flat memory addressing, 8-bit chars, 32-bit ints, two's complement, IEEE floating point etc - and nobody would even notice. In fact, a lot of it likely already does assume some or all of that, just not explicitly.
It would be nice to have an ISO-standard superset of C that catalogs such assumptions. Basically, a "non-DSP, non-mainframe" version of C, that's portable across all modern non-exotic platforms, and provides definitions for as many things that are UB or implementation-defined in standard C as possible.