Skip to main content
GameDev.net gamedev.net
🔒 Locked

uint16_t - uint16_t = signed -_-'

Started by Sik_the_hedgehog Jul 30, 2014 at 4:55 AM 12 replies 5.8k views
Original Post
Sik_the_hedgehog
Sik_the_hedgehog

So, the other day I found again this old comic and it made me want to look for how many instances of swearing there are in my code. Much to my surprise, there's only one (and the game is nearly complete):


      // Copy data from the old map to the new one
      // <Sik> WTF I had to put the explicit cast to unsigned there because
      // apparently substracting two uint16_t will result in a signed type...
      // Gotta love type promotion rules
      for (unsigned x = 0; x <= (unsigned)(src_x2 - src_x1); x++)
      for (unsigned y = 0; y <= (unsigned)(src_y2 - src_y1); y++) {

src_x1, src_y1, src_x2 and src_y2 are all of type uint16_t (also the former two are known to be smaller than the latter two). One would expect the result to stay unsigned, but integer promotion makes it signed int (because those types are smaller). Normally not an issue except because that makes it unpredictable with comparisons (since x and y are unsigned and I'd be comparing them against signed values were it not for the explicit cast).

Anyway, problem solved the obvious way with an explicit cast, and will probably leave it at that since it works just fine and is easy to read. Does anybody here know of a nicer looking solution though? Just curious, I don't really care.

Don't pay much attention to "the hedgehog" in my nick, it's just because "Sik" was already taken =/ By the way, Sik is pronounced like seek, not like sick.
Samith
Samith

Clearly, instead of subtracting you should just add the modular additive inverse!

Off the top of my head, so I'm not sure this is correct:


unsigned inverse(unsigned x)
{
    return (~x + 1);  // i'm mostly sure this is right
}

// arithmetic is over the group of integers modulo 2^n for an n bit integer
// so subtracting is the same as adding the inverse

for (unsigned x = 0; x <= src_x2 + inverse(src_x1); x++)
for (unsigned y = 0; y <= src_y2 + inverse(src_y1); y++) { /* etc */ }

Ta-da! Code now magically readable. Anyone who looks at this code now will immediately know what's going on, and laud your cleverness.

Sik_the_hedgehog
Sik_the_hedgehog

Oh you bastard XD

For the record, that'd work only because the function returns unsigned int (turning the entire sum unsigned int, thereby not resulting in an automatic promotion). If it returned uint16_t the same issue would crop up again.

Don't pay much attention to "the hedgehog" in my nick, it's just because "Sik" was already taken =/ By the way, Sik is pronounced like seek, not like sick.
Ohforf sake
Ohforf sake
If you used (signed) int for x and y, or casted them to int in the comparison, wouldn't that still be enough bits for everything, that src_{x,y}{1,2} can throw at you, but also gracefully handle the cases of src_x2 < src_x1 or src_y2 < src_y1?

Edit: Oh and Samith, I applaud you on not simply doing "return -x;" and seasoned with that lovely comment the code is top notch, but I think the function name really gives away the intend ;-)
Sik_the_hedgehog
Sik_the_hedgehog

Technically, I could just make x and y signed in the first place. It's just that it doesn't make much sense to make them signed if they can't become negative, and especially if they're being compared to supposedly unsigned values (so even though the code would be less cluttered, now it introduces confusion because it seems like it shouldn't work).

Also for the record, it seems compilers have an easier time if the counters used for loops are unsigned.

EDIT: also src_x1 and src_y1 are guaranteed to be smaller than src_x2 and src_y2. The code right before that loop (which isn't posted here) already handles that issue.

Don't pay much attention to "the hedgehog" in my nick, it's just because "Sik" was already taken =/ By the way, Sik is pronounced like seek, not like sick.
Samith
Samith




For the record, that'd work only because the function returns unsigned int (turning the entire sum unsigned int, thereby not resulting in an automatic promotion). If it returned uint16_t the same issue would crop up again.

Ack, didn't read your OP closely enough. I thought the problem was that subtracting two unsigned integers resulted in a signed integer, and so I was trying to avoid any subtracting of unsigned ints by turning the subtract into an add that overflows tongue.png . That way you don't end up with a signed integer, and you can compare safely with your unsigned loop variable. If inverse took a uint16_t and returned a uint16_t that should still work, unless I'm missing something.




Edit: Oh and Samith, I applaud you on not simply doing "return -x;" and seasoned with that lovely comment the code is top notch, but I think the function name really gives away the intend ;-)

Ha! Didn't even think of just returning -x.

Sik_the_hedgehog
Sik_the_hedgehog

Adding two uint16_t values has exactly the same problem, they get promoted to signed int. If the entire substraction happened inside the function and it returned the result though it would work, since the value would be casted back into unsigned on return, and then the comparison would be unsigned against unsigned.

Also coming to think on it I could just make those variables unsigned instead of uint16_t, they're just used for that counter after all... I think I'm using uint16_t only for the sake of consistency with the values they're calculated from (which are also uint16_t).

Don't pay much attention to "the hedgehog" in my nick, it's just because "Sik" was already taken =/ By the way, Sik is pronounced like seek, not like sick.
Samith
Samith




Adding two uint16_t values has exactly the same problem, they get promoted to signed int. If the entire substraction happened inside the function and it returned the result though it would work, since the value would be casted back into unsigned on return, and then the comparison would be unsigned against unsigned.

By god, you're right! I had no idea this happened. I thought subtraction was special and would promote to signed int because the result could technically be negative. I didn't know addition of two u16s would also return a signed int!

Sik_the_hedgehog
Sik_the_hedgehog

The reason for this is simple: uint16_t is smaller than int, so any computation made with it will automatically promote the result to int. This affects both arithmetic and binary operators. Doing this with two unsigned int will not result in the promotion, since the values are already as large as int (they will get promoted only if one of the values is larger than int, so the result will be the largest of the two types).

Yeah, integer promotion rules sucks. I guess this is mostly an issue because you're allowed to mix unsigned and signed values, otherwise automatic promotion would be harmless for the most part (there would be some edge cases like applying ~ then >> but it'd still be way more predictable).

Don't pay much attention to "the hedgehog" in my nick, it's just because "Sik" was already taken =/ By the way, Sik is pronounced like seek, not like sick.
frob
frob

The reason for this is simple: uint16_t is smaller than int, so any computation made with it will automatically promote the result to int

Nope, it is both platform specific and implementation defined.

There were quite a few of those "edge cases".

They were hit hard when people migrated from 16-bit to 32-bit worlds in the mid 1990s. Since the language wasn't really standardized then it was slightly less of an issue. There was much less software back then, and it was much less complex. Since most non-unix people were transitioning over from dos-based programming to windows based programming, the pain was minimal.



When C++11 first hit the scene, the addition of 64-bit integral types broke an awful lot of legacy code.

The worst for my code base at the time was the combination of enum formalization with 64-bit types. We had a bunch of generated enums that were the 32-bit hashes of strings. (Looking up a number is much faster than string comparisons.) Scattered in places through the 10-year-old code base were hard coded numeric constants rather than enum constants. The new formalized enum rules leave a lot open to the implementation, and we had compilers for both Arm and Windows. Naturally they handle things differently.





Note that several of these changed from C++03 to C++11. Code that used to run and generate the expected result no longer generated the same result, yet both compiled on the highest warning level without warnings.

This is from memory so probably has some subtle issues, but do you know the answers to these? Not "think you know" but actually know?


enum SomeNumber { 
  NEGONE = -1,
  ONE = 1,
  UGLYNUM = 0x89ABCDEF,
  OLDSCHOOL_FAKE_NEGONE = 0xFFFFFFFF
 };

SomeNumber enumvar;  // What is this type?  S32? U32? S64? On all the platform's major compilers? Are you certain?

For the rest, we'll assume a compiler that either naturally or through the command line options uses 32-bit enumerated types.

For each one of these, without the optimizer eliminating the test, what underlying type is used for the comparison? Also if applicable, did it change from C++03 to C++11?
For example, is the comparison done as S8, U8, S32, U32, S64, U64, are promotions involved, and if it changed, what was it before and after C++11? Or do the items even compile?

And before you jump over to the godbolt site to try them out, know that their compilers are all on 64-bit PC linux which doesn't give a range of results.


if( ONE == 1 ) {..}   // Before you guess S32, are your really certain?

if( ONE == someS8 ) {..} // Should be easy enough.  ONE is a numeric constant just substituted in, or is it?
if( ONE == someU8 ) {..} 

if( NEGONE == someS8 ) {..} // Are you certain enum constants are a matching type, or are they a magic bit pattern?
if( NEGONE == someU8 ) {..} // Bonus points, name some compilers that handle this implementation defined behavior VERY differently.

if( enumvar == someS8 ) {..} // Are enum variables treated the same as enum constants? Are you certain?
if( enumvar == someU8 ) {..}

if( enumvar == someS32 ) {..} // Let's try that again with a bigger type
if( enumvar == someU32 ) {..}
 
if( NEGONE == someS32 ) {..} // Similar to the S8 and U8 versions above, except not universally...
if( NEGONE == someU32 ) {..}
 
if( OLDSCHOOL_FAKE_NEGONE == someS32 ) {..} // Yeah, that will work.
if( OLDSCHOOL_FAKE_NEGONE == someU32 ) {..} //
 
if( UGLYNUM == someS32 ) {..} // These two should be easy if you passed all the items above. 
if( UGLYNUM == someU32) {..} 
 
// And now the particularly painful "Coding Horrors" entry for cross-platform developers:
if( enumvar == 0x89ABCDEF ) {..} // Doesn't matter your platform, I'm calling both a bug waiting to happen.
if( UGLYNUM  == 0x89ABCDEF ) {..}


Integer conversions and the formalization of implicit conversions of enum types really caused some havoc to us.

Invisible platform-specific promotions and enums-as-patterns that are sometimes S32, or U32 and other times S64 causes lots of fun. In many cases the only way to be certain was to look at the disassembly.

Implementation defined behavior is FUN.

Sik_the_hedgehog
Sik_the_hedgehog

Your problem seems to be more the fact that those details are implementation defined though. Anything that's implementation defined is guaranteed to result in a massive trainwreck that eats babies. This is probably the worst part of C and C++, there's just way too much stuff that's implementation defined.

Don't pay much attention to "the hedgehog" in my nick, it's just because "Sik" was already taken =/ By the way, Sik is pronounced like seek, not like sick.
frob
frob

Your problem seems to be more the fact that those details are implementation defined though. Anything that's implementation defined is guaranteed to result in a massive trainwreck that eats babies. This is probably the worst part of C and C++, there's just way too much stuff that's implementation defined.

Yes. It was in response to something you stated as an absolute truth but is actually IB.

The typedef for int16_t is optional in C++11 and not explicitly specified. The size of an int is implementation defined.

Having so much of it as implementation defined can be seen as one of the language's strengths, as it allows different systems to produce results that have better performance. One of Java's painful problems is floating point which is specified, but the specification is not compatible with a lot of hardware. Many floating point operations must be done in software or use an option that allows you to break from the standardized behavior.

edit/

In each of those examples I gave there are very simple ways to guarantee consistent behavior. You can specify the size. A plain 0x89abcdef is signed long long, but you can drop a UL or L at the end and it becomes a different underlying type.

Sik_the_hedgehog
Sik_the_hedgehog




Yes. It was in response to something you stated as an absolute truth but is actually IB.

I was talking about language design in general when I said that. If you're designing a language you're in control of that kind of pitfalls, e.g. you could make those implicit casts always go to 64-bit, in which case it'd be guaranteed to always behave the same (in before somebody insists we'll have 128-bit integers some day).

Don't pay much attention to "the hedgehog" in my nick, it's just because "Sik" was already taken =/ By the way, Sik is pronounced like seek, not like sick.
SmkViper
SmkViper
Personally I would flag an enum like that as suspicious in code review. If you wanted a specific size you should be using constants with defined sizes rather then an enum (which, inherited from C, has always had implementation defined size with "hacks" to try to force the compiler into "minimum" size). The fact that you're comparing an enum value to a completely different type throws up more red flags, as enums should (ideally) only be compared with their own enum values.

But such is the joy in dealing with legacy code.

C/C++ is very much designed around picking the "fastest" size for types based on the hardware you're compiling for, and only making relative guarantees as to the size of items. Hence why most people make typedefs in their code base for specific sized types, and why later standards have defined the new int8_t family of defines.

I personally have found that explicitly declaring type sizes only really matters in edge cases (and is more important in floats/doubles with precision issues), or when you're optimizing a struct/object's size, or when writing/reading from a disk file.

Topic Locked

This topic has been locked by a moderator. New replies are not allowed.

Sign in to reply to this topic.