-1

To be able to understand overflow in C++ I was trying some code snippets and realized this strange behaviour.

std::uint8_t ofInt = 255;
if ((ofInt + 1) == 0)
  std::cout << "This is not shown in terminal" << std::endl;
char ofChar = 255;
if ((ofChar + 1) == 0)
  std::cout << "This is shown in terminal" << std::endl;

Isn't unsigned integer of 8 bits the same as char? What is the reason of this different behaviour?

11
  • 1
    If your char is signed by default, then (-1 + 1) == 0 is true. Commented Sep 15 at 12:55
  • 1
    "Isn't unsigned integer of 8 bits the same as char?" No. Who told you that? Commented Sep 15 at 12:57
  • 2
    The signedness of char is implementation-defined. Commented Sep 15 at 13:15
  • 3
    One important note if char is signed. Signed number overflows are undefined behavior. You cannot tell for sure what the result will be. Commented Sep 15 at 13:18
  • 1
    @sweenish: There are implicit conversions, but there's no such thing as an "implicit cast". A "cast" is specific syntax that's always explicit. There are implicit conversions, which are conversions that happen without a cast. Commented Sep 15 at 16:21

2 Answers 2

8

There are two parts to this problem.

1. char != std::uint8_t

char is defined to be of size 1 "byte", whatever "byte" means for given computer. Most modern computers have 8-bit bytes, but that's certainly not required by C++. 16-bit would be just as valid.
Also, char can be either signed or unsigned, depending on architecture. Typical x86 would have signed chars.

std::uint8_t on the other hand is always unsigned and 8-bit. If 8-bit variables are not viable on give architecture, this type will not exist.

2. Integer promotion

When performing arithmetic operations (like addition), types in C++ get promoted. Anything smaller than int will get promoted to int and bigger types get promoted to a common type that can handle both values. So internally, what you have looks more like this:

(static_cast<int>(ofInt) + 1) == 0 // 1 and 0 are already of type int

Getting everything together

255 + 1 in realm of ints is 256, there's no overflow. 256 == 0 is obviously false.

Now, it seems your computer has char that is signed and 8-bit. 255 is too big to fit in 8-bit signed variable, so after conversion you get -1.
-1 + 1 is 0 and 0 == 0 is true.

Sign up to request clarification or add additional context in comments.

8 Comments

Is it guaranteed to get -1 ? I thought signed overflow was Undefined Behaviour.
@Fareanor Signed overflow is UB, but std::int8_t x{255} or signed char y{255} is not overflow, it's a narrowing conversion. It was implementation-defined before C++20 and with C++20 or higher I think it's defined to be -1 (but I don't really get what cppreference is trying to tell here). Overflow/underflow only happens as a result of arithmetic operations.
Oh I see, thanks for the clarification
"7-bit or 16-bit would be just as valid." A 7-bit char would not be valid. signed char must be able to represent values from at least -127 to +127, and unsigned char values from 0 to (at least) 255. char must be the same as one of those two.
@JerryCoffin: The representable ranges are derived from the minimum number of bits (8 for char and friends) and the rule that all bits must participate in the representation of the value. Furthermore, now that C++ mandates two's complement for integral types, you can be assured that signed char can also represent -128, as well everything from -127 through 127.
Going all the way back to the C89 standard, the number of bits was derived from the range, not vice versa (largely because at that time, even the use of bits was at least officially optional). Since the basic answer remains the same across versions, I saw no reason not to use information that applies to all versions of the C++ standard (even if I'm one of the few living fossils who still remembers C++98, and before).
You're not the only living fossil. I remember using C well before C89 (and being relieved when an alternative to old-style function declarations came to exist). I also have a copy of the ARM on my bookshelf which I bought shortly after it was first published (and is still useful, on occasion, for answering questions of "why was <something> specified in this way?")
@JerryCoffin: You're right. K&R presented the integral types in terms of sizes. Their second edition does as well, but it also includes the explicit minima and maxima specified in the ANSI standard. The character types in particular are constrained by standard both by size and by range: 1 byte [3.3.3-3.3.5], at least 8 bits [5.2.4.2.1], able to represent every member of the basic execution character set [6.1.2.5], able to represent every member of the source set as a positive value [6.1.2.5], able to represent at least -127 through 127 (for signed) or 0 through 255 (for unsigned).
1

Often char is signed and 8 bits (<note>but it can be more, while sizeof(char)==1 by definition, leading to bytes of more than 8 bits</note>) thus char ofChar = 255; reads as initialize a char with the int value 255, which requires a conversion (see below), and after initialization ofChar is actually -1.

NB: On initialization standard type conversion occurs:

If the destination type is signed, the value does not change if the source integer can be represented in the destination type. Otherwise the result is implementation-defined(until C++20)the unique value of the destination type equal to the source value modulo 2n where n is the number of bits used to represent the destination type(since C++20) (note that this is different from signed integer arithmetic overflow, which is undefined).

Then adding an int constant 1 to your variables, they are submitted to integral promotion meaning that ofInt si converted to an int with value 255 and (ofInt + 1) returns the int 256.

see https://en.cppreference.com/w/cpp/language/operator_arithmetic.html and https://en.cppreference.com/w/cpp/language/usual_arithmetic_conversions.html.

The same happen to ofChar and (ofChar + 1) is actually -1 + 1 thus 0.

This yields the behavior you're observing.


Side note, with appropriate compiler options, you may have an hint about what's happening: https://godbolt.org/z/d8s73YGvx

<source>: In function 'int main()': <source>:10:19: warning: overflow in conversion from 'int' to 'char' changes value from '255' to '-1' [-Woverflow]
10 | char ofChar = 255;


How to check the signedness of char:

#include <cstdint>
#include <format>
#include <iostream>
#include <type_traits>

int main() {
    std::cout << std::format("is uint8_t unsigned? {}\n",
                             std::is_unsigned_v<std::uint8_t>);
    std::cout << std::format("is char unsigned? {}\n",
                             std::is_unsigned_v<char>);
}

LIVE

2 Comments

Re: "Often char is, by default the same as signed char ..." -- yes, and no. char and signed char and unsigned char are three different types. They're all the same size, and often char is signed, so it behaves the same as signed char. But you can write void f(char); void f(signed char); void f(unsigned char); and the compiler will be perfectly happy to see three overloaded functions.
My bad, answer edited. Thx.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.