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?

6
  • 6
    "Most non-trivial C programs involve some level of bitwise manipulation" I find this statement dubious. Most GUI programs, for example, aren't doing bit manipulation. Jul 28, 2016 at 23:58
  • 1
    Every API uses a number of flags ored together. Flags are bit manipulation.
    – rep_movsd
    Jul 29, 2016 at 0:00
  • 1
    Every API uses a number of flags ored together. Flags are bit manipulation., another dubious statement. Technically speaking, ALL operations are bit manipulations.
    – txtechhelp
    Jul 29, 2016 at 0:06
  • If you have 16 flags you need 16 bits in the flag variable...
    – rep_movsd
    Jul 29, 2016 at 0:16
  • 1
    Why is it nitpicking? Every windows program you ever write has CreateWindow() which takes dozens of flags as the window style. fopen() and their ilk take flags. Something as simple as parsing a header of a BMP file takes code that needs to read data into a packed structure. How on earth can you do that without using sized types?
    – rep_movsd
    Jul 29, 2016 at 8:26

5 Answers 5

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.

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.

17
  • 1
    But if your int was 16 bits and you shifted out, you got undefined behaviour
    – rep_movsd
    Jul 29, 2016 at 0:19
  • If you need to parse a file like a BMP, you need to read fixed number of bits into fixed sized integer variables. Would you say that portable C is impossible to write such a program with?
    – rep_movsd
    Jul 29, 2016 at 0:20
  • @rep_movsd: "you need to read fixed number of bits into fixed sized integer variables" Nonsense. You need to be able to read a specific number of bits into integer variables which are big enough to hold at least that many bits. You can read 16-bits of data into an 18-bit integer just fine. Jul 29, 2016 at 0:23
  • Same thing... You want to read a 16 bit value... What type do you use? int? short? long?
    – rep_movsd
    Jul 29, 2016 at 0:25
  • 2
    @NicolBolas Some Arduinos use 16-bit ints, seemingly those based on ATMega microcontrollers.
    – 8bittree
    Jul 29, 2016 at 15:44
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.

4
  • My whole argument is that you don't get brownie points for getting it to compile, you only get it if your programme runs correctly
    – rep_movsd
    Jul 29, 2016 at 0:10
  • @rep_movsd: Well, you can't get it to run if it doesn't compile either. Jul 29, 2016 at 0:15
  • 1
    @rep_movsd It's not like using int will always cause run-time failures. In my example it's a trade-off based on your tolerance of risk. Do you want to have a crack at this dissimilar architecture, or do you want to not compile. I only think the second one is better because I think it's the monetarily cheaper option, not for any grand technical reason. Jul 29, 2016 at 0:27
  • Which is better? a program that compiles and fails at runtime or a program that fails to compile? One blows up rockets and the other merely catches the human error. Once you claim portability, every line of your code needs to be carefully written so that you never overflow ints on any platform! You can't even write a simple for loop in that case for any reasonable count.
    – rep_movsd
    Jul 29, 2016 at 0:56
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.

6
  • 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 system without modification.... Where does the standard guarantee that ints hold -32767 to 32767
    – rep_movsd
    Aug 9, 2016 at 17:24
  • @rep_movsd: see my edit.
    – John Bode
    Aug 9, 2016 at 18:24
  • So the standard does mandate sizes for some types... In which case the argument is moot!
    – rep_movsd
    Aug 9, 2016 at 19:33
  • @rep_movsd: The standard only mandates that the standard types be big enough to hold a minimum range of values; that's not the same as saying a type must be N bits wide. Sizes are implied by the minimum ranges, but not spelled out explicitly.
    – John Bode
    Aug 9, 2016 at 19:46
  • Once again pedantry over reason and common sense. Of course if an integral type holds a range of -32767 to 32767, it HAS to be 16 bits wide. Arguing that it is a implicit property is silly. Your statement is like "Of course it must be 2+2 by the standards, but its not necessary that it be 4" End of the day if you do not know how big your types are, your code is undefined by nature. Whether you get them from the standard or from stdint, or write your own, doesnt matter. You have to know how big something is to put some value into it! Whats so hard to understand about that?
    – rep_movsd
    Aug 9, 2016 at 20:20

Your Answer

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

Not the answer you're looking for? Browse other questions tagged or ask your own question.