Summary of C/C++ integer rules
nayuki.io
nayuki.io
> floating-point number types will not be discussed at all, because that mostly deals with how to analyze and handle approximation errors that stem from rounding. By contrast, integer math is a foundation of programming and computer science, and all calculations are always exact in theory (ignoring implementations issues like overflow).
Integer overflow is no mere implementation issue, any more than errors are an implementation issue with floating-point.
> Unqualified char may be signed or unsigned, which is implementation-defined.
> Unqualified short, int, long, and long long are signed. Adding the unsigned keyword makes them unsigned.
There's an additional point here that's not mentioned: char, signed char, and unsigned char are distinct types, but that's only true of char. That is, signed int describes the same type as int. You can see this using the std::is_same type-trait with a conforming compiler. Whether char behaves like a signed integer type or an unsigned integer type, depends on the platform.
> Signed numbers may be encoded in binary as two’s complement, ones’ complement, or sign-magnitude; this is implementation-defined.
This is no longer true of C++. As of C++20, signed integer types are defined to use two's complement. [0] I don't think C intends to do the same.
> Character literals (in single quotes) have the type (signed) int in C, but (signed or unsigned) char in C++.
That's not correct. In C++, the type of a character literal is simply char, never signed char nor unsigned char. As I mentioned above, whether char is signed depends on the platform, but it's always a distinct type.
> Signed division can overflow – e.g. INT_MIN / -1.
This isn't just overflow, it's undefined behaviour.
> Counting down
> Whereas an unsigned counter would require code like:
> for (unsigned int i = len; i > 0; i--) { process(array[i - 1]); }
That's one solution, but it might be a good place for a do/while loop.
> That's not correct. In C++, the type of a character literal is simply char, never signed char nor unsigned char.
I'd assume the author meant (signed `char` | unsigned `char`) rather than (`signed char` | `unsigned char`).
That's correct, I was going to bring that up too.
This is particularly important because char and unsigned char are special in that they are an exception the aliasing rules. That is, in this function:
float foo(char* cp, float* fp) {
*fp = 7;
return *(float*)cp;
}
/* ... */
float f = 2;
float g = foo((char*)&f, &f);
Then g should end up equal to 7. That's true even if you change the type of the cp parameter to const char*! If you change "char" to "unsigned char" in both places then its behaviour stays the same, but if you change it to "signed char" in both places then it has undefined behaviour (if I've remembered everything correctly). Now I think about it, this conflation of char's use in the C standard has probably prevented a lot of optimisations where code was just using char* for strings rather than for potential aliasing.Another point, which is very related, is that uint8_t and int8_t do not necessarily have to be a typedef for unsigned char / signed char or char, even if char is 8 bits wide. So you could end up with (at least) 5 types that are 8-bit wide!
Combined with the above aliasing rules only applying to char and unsigned char, that means you cannot reliably expect uint8_t to have that aliasing exception. Indeed, gcc originally made a new type of uint8_t and int8_t but that caused so many bugs that they ended up switching them to unsigned char and char (and I think Visual Studio has always done so).
> > Character literals (in single quotes) have the type (signed) int in C, but (signed or unsigned) char in C++.
> That's not correct. In C++, the type of a character literal is simply char, never signed char nor unsigned char.
I was going to bring this up too, although I wouldn't quite say it's outright incorrect because I'm not sure they were making the claim you think they were - it could be interpreted to mean that it's always char in C++ but by the way don't forget that could be a signed or unsigned type (note the lack of monospace font for their use of "signed" and "unsigned"). But probably best not to overanalyse it since they probably didn't know the types were distinct - the main thing is reiterate, as you've done, that it's always `char` regardless of whether that's signed or unsigned.
Don't forget std::byte.
As no good language lawyer discussion should be free from pedantry, there is no such thing as "As of C++20". C++20 is just a new version of the C++ standard. Projects that target C++11 or C++14 or C++17 are all still here and won't go away any time soon, and the respective C++ rule still apply to them. Passing a new revision of the C++ standard changes nothing with regards to which rules actually apply to those projects, unless project maintainers explicitly decide to migrate their projects.
and per ISO rules, older versions are withdrawn (as can be confirmed for C++ here: https://www.iso.org/standard/79358.html) and not to be used anymore: https://www.iso.org/files/live/sites/isoorg/files/store/en/P...
Other reasons why a committee may decide to propose a standard for withdrawal include the following :
▸ ▸ the standard does not reflect current practice or research
▸ ▸ it is not suitable for new and existing applications (products,
systems or processes)
▸ ▸ it is not compatible with current views and expectations
regarding quality, safety and the environmentUnsigned types are quite useful when doing bit twiddling because they don't overflow or have a bit taken up by the sign.
That's essentially their only application. The rest are stupid single-bit memory-size optimizations. As Jens Gustedt noted, it's one of the (many) misnomers in the C language. It should be better called "modulo" instead of "unsigned". Other such misnomers that I recall:
unsigned -> modulo
char -> byte
union -> overlay
typedef -> typealias
const -> immutable
inline -> negligible
static -> intern
register -> addressless
EDIT: found the reference https://gustedt.wordpress.com/2010/08/18/misnomers-in-c/What about when it doesn't make semantic sense to have negative values? Eg for counting things, indexing into a vector, size of things. If negative doesn't make sense, I use unsigned types. Its not about the memory-size in that case.
And yet Java's lack of unsigned integers is considered a major example of its (numerous) design errors.
> No need to enforce your semantics through type, and especially not when the values of the type are trivially particular cases of the values of another type.
Of course not, there's no need for any type at all, you can do everything with just the humble byte.
> The same thing for positive numbers
No?
> You can and should do everything with signed integers
You really should not. If a value should not have negative values, then making it so it can not have negative values is strictly better than the alternative. Making invalid values impossible makes software clearer and more reliable.
> except bitfields, of course.
There's no more justification for that than for the other things you object to.
Maybe I'm spoiled by other languages with more powerful type systems, but this is exactly what I want my types to do! Isn't this why we have type traits and concepts and whatnot in C++ now? If not for semantics, why have types at all, the compiler could figure out what amount of bytes it needs to store my data in, after all.
I use types for two things: to map semantics to hardware (if memory or performance optimization are important, which is rare) and to enforce correctness in my code. You're telling me that the latter is not a valid use of types and I say that's the single-biggest reason I use statically typed languages over dynamically typed languages, when I do so.
But even if that's not the case, why would I use a more general type than I need, when I know the constraints of my code? If I know that negative values are not semantically valid, why not use a type that doesn't allow those? What benefit would I get from not doing that? I mean, why do we have different sizes of integers when all the possible ones I could want can be represented as a machine-native size and I can enforce size constraints in software instead? We could also just use double's for all numbers, like some languages do.
The same ugliness occurs when using unsigned types to store values that happen to be positive. Well, in that case it is even worse, because it is incomplete and asymmetric. What's so special about the lower bound of the possible set of values? If it's an index to an array of length N, you'll surely want an integer type whose values cannot exceed N. And this is a can of worms that I prefer not to open...
If the language allows me to and its an important semantic part of my program, then yes. The same way as I would create types for units that need conversion.
Unless I'm writing low level performance sensitive code, yes, I want to encode as much of my semantics as I can, so that I can catch mistakes and mismatches at compile time, make sure units get properly converted and whatnot.
> What's so special about the lower bound of the possible set of values?
Nothing, I would encode a range if I can. But many things don't have a knowable upper-bound but do have a lower bound at zero: you can't have a negative size (for most definitions of size), usually when you have a count of things you don't have negatives, you know that a dynamically sized array can never have an element index less than 0, but you may not know the upper bound.
Also, the language has limitations, so I have to work within them. I don't understand your objection for using what is available to make sure software is correct. Also, remember that many of the security bugs we've seen in recent years came about because of C not being great at enforcing constraints. Are you really suggesting not to even try?
> And this is a can of worms that I prefer not to open...
And yet many languages do and even C++20 is introducing ranges which kind of sort of fall into this space.
For the same reason, say, in an HTTP server I could store a request as a char* or std::string, but I would definitely create a class that ensures, upon construction, that the request is valid and legitimate. Code that processes the request would accept HTTPRequest, but not char*, so that unverified requests cannot even risk to cross the trust boundary.
This is just enough type semantics to injure oneself.
yes, but understand that, despite the name, what unsigned models in C / C++ is not "positive numbers" but "modulo 2^N" arithmetic (while signed models the usual arithmetic).
There is no good type that says "always positive" by default in C or C++ - any type which gives you an infinite loop if you do
for({int,unsigned,whatever} i = 0; i < n - 1; i++) {
// oops, n was zero, n - 1 is 4.something billion, see you tomorrow
}
is not a good type.If you want a "always positive" type use some safe_int template such as https://github.com/dcleblanc/SafeInt - here if you do "x - y" and the result of the computation should be negative, then you'll get the rightful runtime error that you want, not some arbitrarily high and incorrect number
The correct uses of unsigned are for instance for computations of hashes, crypto algorithms, random number generation, etc... as those are in general defined in modular arithmetic
The code was check-all-pairs, e.g.
for (int i = 0; i < container.size() - 1; ++i) {
for (int j = i + 1; j < container.size(); ++j) {
stuff(container[i], container[j]);
}
}
Which worked just fine for int size, but failed spectacularly for size_t size when size==0.I totally should have caught that one, but I just couldn't see it until someone else pointed it out. And then it was obvious, like many bugs.
While there are a few cases where the compiler tends to do a better job of optimizing signed ints than unsigned ints (generally by exploiting the fact that signed integer overflow is undefined), they are not as fundamental as "num / 2". Being forced to write "num >> 1" all over the place whenever I care about performance is basically a dealbreaker for me in many projects; and I haven't even gotten into the additional safety issues introduced by undefined overflow.
As someone else already said, the so called "unsigned" integers in C are in fact remainders modulo 2^N, not unsigned integers.
While the sum and the product of 2 unsigned integers is also an unsigned integer, the difference of 2 unsigned integers is a signed integer.
The best behavior for a programming language would be to define correctly the type of the difference of 2 unsigned integers and the second best behavior would be to specify that the type of the result is unsigned, but to insert automatically checks for out-of-domain results, to detect the negative results.
As C does not implement any of these behaviors, whenever using unsigned integers you must either not use subtraction or always check for negative results, unless it is possible to always guarantee that negative results cannot happen.
This is a source of frequent errors in C when unsigned integers are used.
The remainders modulo 2^N can be very useful, so an ideal programming language would support signed integers, unsigned integers and modular numbers.
const -> read_only_view is better
> char is always 8 bits wide. int is always 32 bits wide
> Signed overflow is guaranteed to be wrap around. (e.g. INT_MAX + 1 == INT_MIN.)
Are there any current, relevant hardware architectures where this is not true (e.g. bytes are not 8 bits, and integers are not 2's complement)?
E.g. what's the point of "portability" if there is no physical hardware around anymore where those restrictions would apply?
Certainly you can find an architecture which may run some type of C-like language with strange arithmetic rules (e.g. DSPs). I would bet it's harder to find one such architecture where one can run standard C, and impossible to find one which can run standard C++.
After all, who are language nerds to dictate chip manufacturers what the ISA should look like? :P
And it was only in the last 2 decades that everything got dominated by x86...
"Relevant" is in the eyes of the beholder, and its all too easy to no-true-scotsman your way out of existing architectures. I claim that both of these architectures are relevant by virtue of suppliers continuing to make new chips that use them, and system builders continuing to select those chips in new products.
Isn't double required to have more precision than float?
char-- 16 bits
short --16 bits
int --16 bits
long-- 32 bits
long long -- 40 bits
float-- 32 bits
double -- 64 bits
Isn't this in direct contradiction to what the article says?
> long long: At least 64 bits, and at least as wide as long.
So the UB that is associated with overflowing an int is required to efficiently compile loops that use a counter `int i` to index an array. There is a huge number of these loops in the wild.
This problem might be just some unfortunate coincidence with how array indexing is defined in C. I don't understand this deeply, but just wanted to bring it up. I believe I read this on Fabian Giesen's blog.
The classic RISC machines could only perform full-register arithmetic. RISC-V has a small handful of instructions that can accelerate signed subregister arithmetic, but none that accelerate unsigned subregister arithmetic. So, if you need a 32-bit unsigned integer operation to guarantee wrap-around behavior on 64-bit RISC-V, the compiler may have to insert additional zero-extension instruction sequences if it cannot prove the absence of overflow.
For char, not sure, but the problem with signed overflow is not that you can't be sure whether it's 2's complement, it's that the compiler is allowed to assume it won't happen. So, if you read two numbers into 2 ints and add them up, then check for overflow somehow, the compiler will just remove your check while optimizing, since integrr addition can't overflow in a valid program.
In practice compilers sould be considered to follow Murphy's Law: Undefined Behavior will work perfectly fine when on a developer's machine or when observed by any support or QA staff, but will occasionally cause intermittent problems on production machines when observed by users or during demonstrations to executives.
I.e., if the C std says it's 'undefined', it is not to be avoided for portability reasons (hardware, assembler), but it must not be used, end of story. The portability stuff is called 'implementation defined' in C, not 'undefined behaviour'. The problem is that the compiler can (and will!) exploit undefined behaviour rules. E.g., the following code is officially broken (and not just on weird hardware, but everywhere, as defined by the C std):
int saturated_increment(int i)
{
if ((i + 1) < i) { /* if it overflows, do not inc */
return i;
}
return i + 1;
}
The compiler may (and many will) remove the whole if() block, because i+1<i is trivially false, because int cannot overflow (says the C standard).As one can imagine, when compilers started exploiting this, a lot of discussion about sensibility followed. And gcc added -fwrapv among other things.
(And the code would be fine if 'unsigned' was used in stead of 'int', because this is only a problem of signed ints.)
Reminds me of the examples where the code gets compiled in a way where a branch that returns from the function is unintuitively always taken because the compiler was able to detect that there is undefined behavior later in the function and since undefined behavior isn't legal, it assumed that it therefore can never reach there, so the branch must always get taken and the actual condition check got optimised away (IIRC).
So yeah, undefined behavior isn't "implementation defined" nor "unportable" but rather "illegal not allowed wrong code".
There are edge-cases even there. Calling a function generated by a JIT compiler is undefined behaviour, but there's a gentleman's agreement that the compiler won't screw it up for you.
Almost all C/C++ compilers promise that floating-point division-by-zero results in NaN (the IEEE 754 behaviour), but according to the C/C++ standards themselves, it's undefined behaviour.
You're right though that in general, one should not be complacent about UB.
Though you're not writing C/C++ in that case. You're writing "C/C++ for that particular architecture, ABI, OS and compiler".
In general C/C++, if your code is correct every present and future, known and unknown compiler is supposed to generate a correct executable. If they don't, they have a bug. You can pretend to be smarter and go UB, but then the responsibility shifts on you, you have (in principle) to validate each compiler and environment and you can claim no bug on anybody other than you.
For 'char has 8 bits': the bitwidth is 'implementation defined' in C. If you know your target architectures, you can assume it's 8 bits, because it's indeed a question of portability.
For 'int must not overflow': this is 'undefined behaviour' in C. You must not do it, regardless of what you know about your target architectures, because this is a language level prohibition.
Thankfully, in addition to what MaxBarraclough helpfully pointed out, every (u)intN_t type provided by <stdint.h> is guaranteed to use two's complement even in C99.
What I meant is undefined behaviors allow compilers to optimize in a way that would not be possible otherwise. So, it might be a deliberate decision back then, to leverage performance. I don't know, just an idea.
Once you’ve got your C code doing safety checks with data types that won’t break under the littlest change, the code becomes much slower than code golf would suggest. A common example is passing void pointers everywhere. You either check every call every time (aka dynamic typing) or rush everything on the idea that the programmer understands the system completely and never forgets or messes up. Better types give you all the speed AND all the safety here.
This is false. For a long time C performance used to be inferior to Fortran, which is arguably safer than C. It's hilarious that the strict aliasing and `restrict` keyword was born out of making C on par with Fortran and UB became a major issue to C programmers as a result!
Does anybody know any paper for bugs per lines of code for different languages or something similar?
And on practice, C/C++ developers are among lower-paid programmers - probably because "banging out ideas" and producing actual programs that actually work, are valued more than language elitism.
> sizeof(T) represents the number of 8-bit bytes (octets) needed to store a variable of type T.
That's a misconception I had and I've never run into a problem. What's a platform where sizeof works differently?
Also, what's the reasoning for sizeof to be an operator rather than a function?
If you don't want to bother supporting platforms where byte is not 8-bit (a reasonable choice I would say), use `int8_t`/`uint8_t` instead. Those types won't exist on platforms that don't have 8-bit bytes.
It could have used a different syntax though. Ada has a special syntax for compile-time inquiries like this, so there's no way to confuse them with function calls. Ada calls these attributes.
https://en.wikibooks.org/wiki/Ada_Programming/Attributes#Lan...
You'll have the issue that, as one of the commenters explained above, `char` is its own thing, independent and separate from `signed char` and `unsigned char` to say nothing of `int8_t` and `uint8_t`. This means that while you can use your own thing for your own functions you can not do so if your values have to interact with libc functions (or most of the ecosystem at large).
If you only want to support platforms using 8-bit chars, you should check CHAR_BIT. That is actually reliable and correct.
Well, the beauty of C is that you can have that too, if you wish, and you have many options to choose from.