9

I have been having a huge and heated argument with someone on a slack group - the debate is this :

My argument

  • Portable code is that which compiles with various compilers and runs exactly the same on various architectures without modification.
  • If your code depends on the sizeof(T) of an integral type, in order to keep it portable you should use the types in stdint.h like uint8_t or uint32_t
  • Most non-trivial C programs involve some level of bitwise manipulation, so everyone should know about stdint.h

His argument

  • The C standard says "Use standard types for maximum portability"
  • uint32_t etc. are not guaranteed to be present on all architectures, so don't use any of them
  • stdint.h is C99 so one should avoid it, since embedded systems may only support only C89

My counter is that unless you know the sizes of your types, your code can be fundamentally broken, and even if you do not use stdint.h there is always some header file that has typedefs to explicitly sized types (for example Windows.h declares UINT8, UINT32, DWORD and so on)

The only reply I get is that if you use uint32_t and so on it is not portable according to the C standard. If runtime behaviour changes due to type sizes, that doesn't mean the code is not portable.

Ordinarily I would let this go, but this is in a forum where a lot of budding C and C++ programmers come for guidance, many of whom I know personally. Also this guy claims to be a professional trainer of C programmers, so that worries me all the more.

In what other way can I convince this guy that if you use the basic types like int or short, your code will not be portable?

It boils down to the definition of "portable" - does it merely compile on various compilers, or does it also have to run the same way on every platform it compiles on.

I would have thought runtime portability is what matters - what are your comments?

Nicol Bolas
  • 12,043
  • 4
  • 39
  • 48
rep_movsd
  • 209

5 Answers5

8

"Way back when", I had to write highly portable code that needed exact type sizes for a small-device database that ran on anything from 8 bit to 64 bit platforms. We had a coding standard that mandated exact sizes for all variables; this approach made it easier to pass MISRA (among other goodnesses).

To make this work, I wrote and maintained my own "stdint.h" for c89 builds, with the appropriate #ifdef's to use the standard one or "mine".

As for whether using your own stdint.h is "portable", as long as you have appropriate ifdefs, it's about as portable as it can be. Without stdint.h, there are no platform-independent sized types in C, so you "gotta do something". You're right that int, short, etc do NOT have standard sizes in C, and using them is not portable if you're relying on them to be specific sizes.

6

I think you both might be trying to prove a moot point; "portable" is a common misnomer, often times you don't even need to leave a specific platform to have "portability" break (look at any major upgrade for any major OS).

Your arguments are based solely on bits. If this is the case, "portability" can break when going between architectures that have different bit widths (look at CHAR_BIT).

Additionally, a lot of the standard (and many languages for that matter) have a clause for a lot of the semantics that state something is implementation defined, which automatically means it's not "portable" (as you can't be guaranteed it will be available).

I think if you want to get really technical, you should break down the meaning of the word "portable": to port something is to move from one platform to another.

If the code is port-able, then I can port the code from one platform to another without incident (i.e. the code will run as expected). But that doesn't mean I have to change the compiler. Changing the compiler automatically assumes I can no longer guarantee that my code will "port" without incident.

If I am trying to guarantee that my code works as expected across compilers, then I'm not testing portability between systems but compliance between compilers with the specific C standard that I'm coding to (i.e. C89/C99/ISO/ANSI/Embedded C, etc.).

C is not 100% portable specifically because of what it's meant to do; be as close to the hardware as possible.

txtechhelp
  • 272
  • 1
  • 5
5

Portability is always within some range of platforms. C is not a perfectly portable language; it is possible to build machines on which you cannot implement C (or on which you cannot implement C reasonably well). So the question of portability is always one of what range of platforms you support.

By using C, you support some subset of platforms that have working C compilers. Obviously. But what you do with C can narrow that support.

If your code depends on the sizeof(T) of an integral type, in order to keep it portable you should use the types in stdint.h like uint8_t or uint32_t

If you write code that depends on the size of a type, you are not writing code that is as portable as C itself. As you have been informed, uint8_t and such types are not required by the C standard. By any C standard; they're optional features of C99 and C11. And C++11 and above, in case you were wondering.

But portable "as far as the standard is concerned" is not necessarily the end-all of portability. There are after all a lot of platforms which can and do support those types. Your code can be portable within those platforms.

What you are being warned about really is not so much reliance on the sized types. What you're being warned about is reliance on type size. It is that which limits your platforms, because C doesn't define type sizes. C allows the individual platforms to do so.

Therefore, code which relies on having a fast, 32-bit integer type is by its very nature not portable to platforms that don't have such things. As an example of this, Vulkan bills itself as a cross platform graphics API. And it is. But Vulkan defines all of its types in terms of the stdint.h sized integer types. Why?

Because if a platform can't support them, then the platform cannot support Vulkan period. So there's no point in pretending that Vulkan can run on anything that C can.

That's not wrong; it's simply setting your limitations based on the needs of your code. You are frequently sending data structures directly to and from Vulkan, and you need to be able to match what implementations expect/require. So your C platform needs to support types that can match these.

At the same time, if you're not doing something that explicitly needs a specific size of integer, then you're doing yourself a disservice by using an explicitly sized integer type when an int would do. The int_leastXX_t types are required, and they can handle most problems regarding integer ranges.

In what other way can I convince this guy that if you use the basic types like int or short, your code will not be portable?

Using these types will only be non-portable if you do non-portable things with them.

For example, bit manipulation is well-defined in C. The specific number of bits in int is not. So the following code:

unsigned int i = 1 << 19;

Is perfect well-defined... so long as sizeof(unsigned int) is 20 bits or more on that platform. So any platform where this is true will be fine.

By contrast:

uint32_t i = 1 << 19;

Is only well-defined code if the platform supports uint32_t. If it has an unusual byte size (9 and 18 bits are not unknown byte sizes), then it cannot support uint32_t. And therefore that code cannot work. But it may have worked for the first case, because int might have been 36 bits in size.

By restricting yourself to the specific sized integer types, you are also restricting yourself to platforms that support those sized integer types. If you use the fundamental integer types, you are restricted to platforms where those types are "big enough" for whatever you use them with.

So which one is the more portable? It's hard to argue that the sized one is the more portable, when the unsized one can run on anything (which supports C) that has integers that are big enough. Whereas the sized one is restricted only to platforms that can support 32-bit integers. Even though that code isn't actually using 32 bits.

I suppose one advantage of the latter is that, if sized types are not supported, you get a hard compile error. By contrast, if int is not big enough, you get invisible implementation-defined behavior. C++11 got a solution to that problem with static_assert, which could prevent compilation on an arbitrary compile-time thing. Like the size of an int being big enough for a particular use.

Nicol Bolas
  • 12,043
  • 4
  • 39
  • 48
4

Will the platform have them?

Well fixed width integer types have been in each language's respective standard since C+11 and C99. I'm not sure your opponent's first argument makes sense.

They are however optional. They don't exist if there is no native integer type of that width. That's a completely valid objection.

Let's say you're writing on a platform where the native int flavour is 32 bits, and later target a 64 bit platform with no native 32 bit type. If you use int32_t your code won't compile. If you use int it will, and as long as you didn't make any 32 bit assumptions it will run fine. Win for int, it's more portable. On the other hand, not-compiling is about the most transparent, quickest appearing and easiest to fix way a program can fail. If you did make a 32-bit assumption and used int, you have a horrible platform specific runtime bug. Fun. Win for int_32t.

So it's a trade-off. Using fixed width integers will mean your code complies on less platforms, but you can have more confidence it will work as expected once it does. Using non-fixed width integers means it will compile on more platforms, but you'll have to do a whole bunch more QA on these different platforms. Which is more portable depends on your idea of what portability means. I agree with you, because I'm risk averse and want a quiet life.

C89?

Fair enough, you're opponent is sort of right in a very academic sense. Technically why not make it compile on K&R C. Even after C89 came out, people used to write with K&R compatibility in mind. We stopped because we didn't want to give up a lot of cool features that make code better. Imho, unless you have an known need to target old compilers, writing for C89 is insane (Writing for pre C+11 is even crazier). This is the problem with un-grounded academic conversations.

Most non-trivial C programs involve some...

Maybe. I tend to assume that I've done the stupidest possible thing somewhere in the code, because I don't like bugs. So I'll use fixed width for safety. But C programs vary wildly. You can't even say all non-trivial programs use pointers. So this is the sort of statement you can expect to get shot down.

Nathan
  • 1,309
1

As soon as your code has to make any assumptions about type sizes or representations beyond what the language definition guarantees, you limit its portability. If your code assumes that an int object must be exactly 32 bits wide, then it won't be trivially portable1 to systems with 16-bit ints, or systems where CHAR_BIT is not 8, etc.

OTOH, if you only assume that an int must be able to store the range of values [-32767..32767], then your code is portable to any (conforming) system without modification.

Fixed-size types make life easy on platforms where they are supported, but they won't make your code more portable in general.

Edit

For rep_movsd, chapter and verse:

5.2.4.2.1 Sizes of integer types <limits.h>

1     The values given below shall be replaced by constant expressions suitable for use in #if preprocessing directives. Moreover, except for CHAR_BIT and MB_LEN_MAX, the following shall be replaced by expressions that have the same type as would an expression that is an object of the corresponding type converted according to the integer promotions. Their implementation-defined values shall be equal or greater in magnitude (absolute value) to those shown, with the same sign.
...
— minimum value for an object of type int
   INT_MIN -32767 // −(215 − 1)

— maximum value for an object of type int
   INT_MAX +32767 // 215 − 1

...
6.2.5 Types
...
5     An object declared as type signed char occupies the same amount of storage as a ‘‘plain’’ char object. A ‘‘plain’’ int object has the natural size suggested by the architecture of the execution environment (large enough to contain any value in the range INT_MIN to INT_MAX as defined in the header <limits.h>).

Emphasis added.

On most modern architectures int is at least 32 bits wide, but it's not universal. I got bitten by that way back in the '90s; we were working on code that had to run on MacOS 8 and Win 3.1. I wrote some code that assumed a 32-bit int that worked fine on the Mac but blew up on the Windows machine, which still used 16-bit int.


  1. Meaning you can simply recompile for the new target without changing the source.

John Bode
  • 11,004
  • 1
  • 33
  • 44