You should prefer signed types for computing (if not storing) sizes and for pointer arithmetic, since they are more forgiving with underflow.
Using an integer, in the 1000s of for loops I've written, none get even remotely close to the billions - it is optimizing for a 1 in a million case, and if I know something can run into the billions of iterations I'm going to pay more attention to anyway. I've seen 0 occurrences of bugs relating to this kind of overflow.
Using a size_t, it is effectively an unsigned integer that risks underflowing which can easily cause bugs like infinite loops if decrementing or other bugs if doing any index arithmetic. I've seen many occurrences of these kind of bugs.
Any such code submitted to one of our projects would be rejected or fixed to use the proper type.
I've not introduced a security bug in every for loop I've written. What I've written shouldn't be controversial, just take a look at Googles style guide:
"We use int very often, for integers we know are not going to be too big, e.g., loop counters. Use plain old int for such things. You should assume that an int is at least 32 bits, but don't assume that it has more than 32 bits. If you need a 64-bit integer type, use int64_t or uint64_t.
For integers we know can be "big", use int64_t.
You should not use the unsigned integer types such as uint32_t, unless there is a valid reason such as representing a bit pattern rather than a number, or you need defined overflow modulo 2^N. In particular, do not use unsigned types to say a number will never be negative. Instead, use assertions for this.
If your code is a container that returns a size, be sure to use a type that will accommodate any possible usage of your container. When in doubt, use a larger type rather than a smaller type.
Use care when converting integer types. Integer conversions and promotions can cause undefined behavior, leading to security bugs and other problems."
One of these days, the compiler will do something surprising to one of your expressions involving signed integer overflow, like converting x < x + 1 to true. Or it'll delete a whole loop because it noticed that your code is guaranteed to trigger an out-of-bounds array read, e.g.: https://devblogs.microsoft.com/oldnewthing/?p=633
> If you do that you'll be fine.
I would not trust code written based on the methodology you described. However, if you also add -fsanitize=undefined,address (UBSan and ASan) and pass those tests, then I would trust your code.
Are you really, though? I would argue that it's a matter of perspective and/or semantics.
The Linux kernel is built with -fwrapv and with -fno-strict-aliasing, and uses idioms that depend on it directly. We can surmise from that that the kernel must be:
1. Exhibiting undefined behavior (according to a literal interpretation of the standard)
OR:
2. Not written in C.
Either way, it's quite reasonable to wonder just how much practical applicability your statement really has in any given situation -- since you didn't have any caveats. It's not as if the kernel is some esoteric, obscure case; it's arguably the single most important C codebase in the world. Plus there are plenty of other big C codebases that take the same approach besides Linux.
Lots of compiler people seem to take the same hard line on the issue -- "the C abstract machine" and whatnot. It always surprises me, because it seems to presuppose that the only thing that matters is what the ISO standard says. The actual experience of people working on large C codebases doesn't seem to even get acknowledged. Nor does the fact that the committee and the people that work on compilers have significant overlap.
I'm not claiming that "low-level C hackers are right and the compiler people are wrong". I'm merely pointing out that there is a vast cultural chasm that just doesn't seem to be acknowledged.