4 ms·
I currently follow "use unsigned types", but add `-Wall -Wextra -Wconversion -Wsign-conversion` (and on Clang you also need `-Wtautological-unsigned-zero-compar
by nyanpasu64 5y ago
I currently follow "use unsigned types", but add `-Wall -Wextra -Wconversion -Wsign-conversion` (and on Clang you also need `-Wtautological-unsigned-zero-compare`) and keep my code at zero warnings, because otherwise C++ (and C)'s implicit conversions are a footgun. After I've added these warnings, I don't think I've hit any bugs due to mixing unsigned and signed quantities (though it's definitely added work to keep track). I did hit "accidentally allowed an unsigned value to go negative/overflow" twice so far, once on MSVC (I don't know a good fix) and once on Clang (that's when I added `-Wtautological-unsigned-zero-compare`, to warn me when I write a check for a value which I think is negative, but is actually UINT_MAX).
One reason I picked this route is because Rust only lets you index arrays by usize and not isize, so I chose to extend this "unsigned for non-negative quantities" philosophy to C++. Additionally, std::vector::size() is size_t and unsigned, so I decided to follow. Because of the mixture of unsigned and signed indexing (and 32 vs. 64 bit values) across different libraries, I decided to turn on warnings so I know where incompatibilities lie. Rust outdoes C++ because it makes incompatible integer widths hard errors, comes with the equivalent of `-Wtautological-unsigned-zero-compare` out of the box, and has runtime checking (in debug mode) which panics if you decrement an unsigned value past 0.
I can understand "signed by default" even though I disagree and prefer not to use it for my own code. And I think 31-bit integers are a good idea if you don't need the range of an unsigned integer. I wish Rust had a type for "31-bit integer with negative values serving as a niche for enum cases" (though my concern is that a zero-overhead mutation API without runtime checks, combined with niche filling, would be UB since you can decrement 0 to -1 and effectively transmute an enum holding a u31).
Also signed integers aren't fully trouble-free, and can still overflow for very large differences (though that's less likely than 2 - 3). The Stack Overflow post mentions:
> Want to find the "delta" between two unsigned indexes into a file? Well you better do the subtraction in the right order, or else you'll get the wrong answer.
Well naive signed integer subtraction can be UB as well.
constexpr int f() {
return 0x7fffffff - -0x7fffffff;
}
constexpr int x = f();
<source>: In function 'constexpr int f()':
<source>:4:23: warning: integer overflow in expression of type 'int' results in '-2' [-Woverflow]
4 | return 0x7fffffff - -0x7fffffff;
| ~~~~~~~~~~~^~~~~~~~~~~~~
<source>: At global scope:
<source>:7:20: in 'constexpr' expansion of 'f()'
<source>:7:21: error: overflow in constant expression [-fpermissive]
7 | constexpr int x = f();
| ^
Compiler returned: 1
As jepler mentioned (https://news.ycombinator.com/item?id=28983587 https://news.ycombinator.com/item?id=28983587), if you want to handle arbitrary difference without overflow, you may need to compute the absolute difference and the sign separately. Sadly it's a massive pain to accomplish.
I guess this case (large numbers) is very rare compared to accidentally subtracting 3 from 2, and addition also risks overflow. And if you're using subtraction for deltas, then restricting file sizes to 2^31 - 1 or 2^63 - 1 makes differences fit in int32_t or int64_t (64-bit ptrdiff_t).
- BeeOnRope 5y ago> Well naive signed integer subtraction can be UB as well. Sure, but the point is that you have to get overflow due to high magnitude of the involved integers. So yeah INT_MAX - -INT_MAX is UB, but you have to have quantities up around INT_MAX for this to be a problem. For the unsigned case you just have to have quantities around 0. Now, having quantities around INT_MAX isn't necessarily that unusual, but what seals the deal for me (I wrote that SO answer) is that 64-bit values are becoming more ubiquitous, and there we can guarantee in some sense that many quantities won't reach their 2^63 limits any time soon. So I definitely thing "signed by default" is much more of a slam dunk if it is paired with "64-bit by default". Note that though I'm a proponent of signed by default, in practice I find it hard to write this way in C and C++ because you are constantly fighting the impedance mismatch between the language, size_t, size(), etc and your own rule. So signed by default is definitely more palatable in languages which made that same decision for their builtins and standard library.
- jart 5y ago> So I definitely thing "signed by default" is much more of a slam dunk if it is paired with "64-bit by default". Yes yes and yes. There is nothing that would make me happier than if GCC and Clang gave us the freedom to choose an ILP64 data model. In that case, we wouldn't need prototypes anymore and we could restore much of the original intent behind the design of C.
- gpderetta 5y agoFWIW, I think these days unsigned size(), although something that conceptually makes sense, is generally considered a mistake although not something that the committee can easily fix.
- deleted 5y ago[deleted]
- nyanpasu64 5y agoIn C++, if I use unsigned variables whenever possible, the users get an invariant for free that these indexes are never negative (though C++ has no overflow checking to catch "negative" overflowing values at time of creation). Would a language with signed sizes and indexing lose out on this property, and require data structure and algorithmic/business logic to compulsively check all outside input to ensure these values are non-negative (because negative values are in the type's domain, but invalid for many algorithms)? What about compulsively checking internal state (which may/not have the intended and/or enforced invariant that internal state values are non-negative)?