Hello, and welcome to C++ Weekly. I'm your host, Jason Turner. Oh goodness! In this episode I
am going to attempt to convince you that all of your small integer operations are broken.
This episode of C++ Weekly is sponsored by me. Be sure to check out the video description for
important links about upcoming events, sponsoring the channel with Patreon, buying copies of my best
practices and puzzler books, and how to contact me if you are interested in on-site training or
remote code reviews. Okay, so depending on your perspective, you are either going to be
surprised or not that 0 minus 1 in this example returns -1. Generally speaking, if we see
"0 - 1," we kind of expect that to be a result of -1, right? That's not really surprising,
but these are unsigned "uint8_t," so let's look at it this way:
If I do it this way I get 255 returned, and that's because an unsigned 8-bit integer is simply going
to wrap back around. This is defined behavior. If I take 0 and subtract 1, it's going to go to
max 'int' for its size, which is positive 255; and that's probably what you were expecting to see
if you were familiar with unsigned integer arithmetic and you use these
small integers. So what happens though if I change this type to 'auto'?
I'm back to having -1 as my result, and this is incredibly frustrating to me. The reason here
is because of the result of subtracting two unsigned 8-bit integers.
So what we can see is this little 'i' right here. This is an 'int',
the result of subtracting two 'uint8_t's' is an 'int',
and I can with a 'static_assert' here ask the compiler, "Is, in fact, the type of the result
'int'?" and it is. This compiles. Well surely you would use this as an argument to not use 'auto',
but it gets worse. Let's just say I want to go ahead and not use 'auto', I'm going to be smart.
Now the compiler is not going to compile this, because the type of result is no longer 'int'.
That's good, let's just comment that out. We can see for our own curiosity's sake here that this
is ('h' is) the mangled name of 'uint8_t'. It's the same as an unsigned char.
Now I have lots of warnings enabled in my code. Warnings that I know most of
you don't have enabled because you see something like this and you're like,
"I don't care. This is a conversion from an 'int' to 'uint8_t'. Whatever. Do your job compiler!"
So you are going to disable -Wconversion, and if you're paying attention you'll see here that the
compiler has continued to return 255. Why the heck is it returning 255? It should be returning 255
divided by 2, right? Because if I have 0 minus 1 I get negative 1, or 255 for uint8_t.
I do a right shift by 1 bit, I should get 255 divided by 2. That's what a right shift does.
I can do right shift by three. Any idea what I'm going to get? I'm going to get 255 again.
The only way to get this to give me the correct answer is to make this a 'uint8_t'
and then do the shift right because this implicit promotion to an 'int' right here
means that (in) all of these shift rights we're doing an arithmetic shift right (on most CPUs).
An arithmetic shift right does a sign extend of the sign bit on the leftmost position. So let's
go ahead and illustrate this really quickly. If I have a signed integer that is represented by
these seven bits and I want to do a right shift by one,
we're going to get this result; and that is because it's going to maintain its current sign
in the case of a signed integer. If, on the other hand, I have unsigned and I do something like this
and try to shift this by one to the right,
then I get this result, and this is the exact behavior that we're seeing.
If I don't have this explicit 'static_cast' right here I may as well be just saying, "This is an
integer." An integer is a signed number, and it's going to do this signed, or integer, arithmetic on
the shift right, and it is going to extend that sign bit. But that's not what I wanted. I wanted
to actually do unsigned integer arithmetic because that's what I expected was the result
of subtracting two unsigned integers from each other. That should be an unsigned value. This
IS defined behavior in the standard. I want to maintain that defined behavior.
The compiler says, "No. You actually want signed integer arithmetic." So if we go back we have more
levels of pain. Ah, okay good - we actually don't have any extra pain at the moment. That is nice
to see, but it is not unexpected that you might actually end up having to do things like this
throughout your code base to actually get the correct result. So right now if you do not have
-Wconversion enabled and you're doing arithmetic on small integer types - things that have these
implicit promotions to 'int', then your code is probably broken. You probably have
subtle bugs lurking through your code where you're getting the wrong answers, and this is incredibly
annoying. There are solutions however. I have mocked up but not yet completed
something like a 'non_promoting_int' wrapper class that has all of these 'operator+' and everything
implemented with proper overloads for us so that you can do all of these things and then
never have to worry about getting these promotions that you don't want.
Then you would end up with something like this
and you could use your own "non_promoting uint8_t" type throughout your code base. There's still some
questions that I have about, you know, what happens if you add a unit8 with a uint16? Do
we always want to return a 16? Do we not want to allow that have to make it explicit for
all of our use cases? I'm not entirely sure, but implementing this is an exercise for the reader;
but if you don't implement it your code is probably broken so I suggest that you look
into this and look for other safe integer types and non_promoting integer types that
happen to exist. There are other libraries out there with complete versions of these.
So good luck in fixing your integer arithmetic in your code and be sure to subscribe.