Search

4.6 — Fixed-width integers and size_t

In the previous lessons on integers, we covered that C++ only guarantees that integer variables will have a minimum size -- but they could be larger, depending on the target system.

Why isn’t the size of the integer variables fixed?

The short answer is that this goes back to C, when computers were slow and performance was of the utmost concern. C opted to intentionally leave the size of an integer open so that the compiler implementors could pick a size for int that performs best on the target computer architecture.

Doesn’t this suck?

By modern standards, yes. As a programmer, it’s a little ridiculous to have to deal with types that have uncertain ranges. A program that uses more than the minimum guaranteed ranges might work on one architecture but not on another.

Fixed-width integers

To help with cross-platform portability, C99 defined a set of fixed-width integers (in the stdint.h header) that are guaranteed to have the same size on any architecture.

These are defined as follows:

Name Type Range Notes
std::int8_t 1 byte signed -128 to 127 Treated like a signed char on many systems. See note below.
std::uint8_t 1 byte unsigned 0 to 255 Treated like an unsigned char on many systems. See note below.
std::int16_t 2 byte signed -32,768 to 32,767
std::uint16_t 2 byte unsigned 0 to 65,535
std::int32_t 4 byte signed -2,147,483,648 to 2,147,483,647
std::uint32_t 4 byte unsigned 0 to 4,294,967,295
std::int64_t 8 byte signed -9,223,372,036,854,775,808 to 9,223,372,036,854,775,807
std::uint64_t 8 byte unsigned 0 to 18,446,744,073,709,551,615

C++ officially adopted these fixed-width integers as part of C++11. They can be accessed by including the cstdint header, where they are defined inside the std namespace. Here’s an example:

The fixed-width integers have two downsides: First, they are optional and only exist if there are fundamental types matching their widths and following a certain binary representation. Using a fixed-width integer makes your code less portable, it might not compile on other systems.
Second, if you use a fixed-width integer, it may also be slower than a wider type on some architectures. If you need an integer to hold values from -10 to 20, you might be tempted to use std::int8_t. But your CPU might be better at processing 32 bit wide integers, so you just lost speed by making a restriction that wasn’t necessary.

Warning

The above fixed-width integers should be avoided, as they may not be defined on all target architectures.

Fast and least integers

To help address the above downsides, C++ also defines two alternative sets of integers.

The fast type (std::int_fast#_t) provides the fastest signed integer type with a width of at least # bits (where # = 8, 16, 32, or 64). For example, std::int_fast32_t will give you the fastest signed integer type that’s at least 32 bits.

The least type (std::int_least#_t) provides the smallest signed integer type with a width of at least # bits (where # = 8, 16, 32, or 64). For example, std::int_least32_t will give you the smallest signed integer type that’s at least 32 bits.

Here’s an example from the author’s Visual Studio (32-bit console application):

This produced the result:

fast 8: 8 bits
fast 16: 32 bits
fast 32: 32 bits
least 8: 8 bits
least 16: 16 bits
least 32: 32 bits

You can see that std::int_fast16_t was 32 bits, whereas std::int_least16_t was 16 bits.

There is also an unsigned set of fast and least types (std::uint_fast#_t and std::uint_least#_t).

These fast and least types are guaranteed to be defined, and are safe to use.

Best practice

Favor the std::int_fast#_t and std::int_least#_t integers when you need an integer guaranteed to be at least a certain minimum size.

Warning: std::int8_t and std::uint8_t may behave like chars instead of integers

Note: We talk more about chars in lesson (4.11 -- Chars).

Due to an oversight in the C++ specification, most compilers define and treat std::int8_t and std::uint8_t (and the corresponding fast and least fixed-width types) identically to types signed char and unsigned char respectively. Consequently, std::cin and std::cout may work differently than you’re expecting. Here’s a sample program showing this:

On most systems, this program will print ‘A’ (treating myint as a char). However, on some systems, this may print 65 as expected.

For simplicity, it’s best to avoid std::int8_t and std::uint8_t (and the related fast and least types) altogether (use std::int16_t or std::uint16_t instead). However, if you do use std::int8_t or std::uint8_t, you should be careful of anything that would interpret std::int8_t or std::uint8_t as a char instead of an integer (this includes std::cout and std::cin).

Hopefully this will be clarified by a future draft of C++.

Warning

Avoid the 8-bit fixed-width integer types. If you do use them, note that they are often treated like chars.

Integer best practices

Now that fixed-width integers have been added to C++, the best practice for integers in C++ is as follows:

  • int should be preferred when the size of the integer doesn’t matter (e.g. the number will always fit within the range of a 2 byte signed integer). For example, if you’re asking the user to enter their age, or counting from 1 to 10, it doesn’t matter whether int is 16 or 32 bits (the numbers will fit either way). This will cover the vast majority of the cases you’re likely to run across.
  • If you need a variable guaranteed to be a particular size and want to favor performance, use std::int_fast#_t.
  • If you need a variable guaranteed to be a particular size and want to favor memory conservation over performance, use std::int_least#_t. This is used most often when allocating lots of variables.

Avoid the following if possible:

  • Unsigned types, unless you have a compelling reason.
  • The 8-bit fixed-width integer types.
  • Any compiler-specific fixed-width integers -- for example, Visual Studio defines __int8, __int16, etc…

What is std::size_t?

Consider the following code:

On the author’s machine, this prints:

4

Pretty simple, right? We can infer that operator sizeof returns an integer value -- but what integer type is that value? An int? A short? The answer is that sizeof (and many functions that return a size or length value) return a value of type std::size_t. std::size_t is defined as an unsigned integral type, and it is typically used to represent the size or length of objects.

Amusingly, we can use the sizeof operator (which returns a value of type std::size_t) to ask for the size of std::size_t itself:

Compiled as a 32-bit (4 byte) console app on the author’s system, this prints:

4

Much like an integer can vary in size depending on the system, std::size_t also varies in size. std::size_t is guaranteed to be unsigned and at least 16 bits, but on most systems will be equivalent to the address-width of the application. That is, for 32-bit applications, std::size_t will typically be a 32-bit unsigned integer, and for a 64-bit application, size_t will typically be a 64-bit unsigned integer. size_t is defined to be big enough to hold the size of the largest object creatable on your system (in bytes). For example, if std::size_t is 4 bytes wide, the largest object creatable on your system can’t be larger than 4,294,967,295 bytes, because this is the largest number a 4 byte unsigned integer can store. This is only the uppermost limit of an object’s size, the real size limit can be lower depending on the compiler you’re using.

By definition, any object larger than the largest value size_t can hold is considered ill-formed (and will cause a compile error), as the sizeof operator would not be able to return the size without wrapping around.


4.7 -- Introduction to scientific notation
Index
4.5 -- Unsigned integers, and why to avoid them

267 comments to 4.6 — Fixed-width integers and size_t

  • empleat

    What is this terminology? One time: it is variable size, other variable width, another variable length - google found nothing. Or variable range!!! Can you please tell me what is what? It can refer either to size of bytes variable will take, or to its range like -127 - 128. Since you use these terms interchangeably - I lost track what is what. I checked past topic like sizeof and couldn't find what variable length means.

    • nascardriver

      Length = Number of items in a container (eg. a list) (covered later)
      Size = Number of bytes the object takes up (Containers use "size" to refer to the number of items they contain)
      Width = Number of bits the object takes up (Size*8)
      Range = Minimum and Maximum value a type can store (Only for numeric types)

      A `std::uint8_t` has
      size=1
      width=8
      range=[0,255]

  • yousuf

    Please take a look at this line of the last paragraph, "For example, if std::size_t is 4 bytes wide, the largest object creatable on your system can’t be larger than 4,294,967,295 bytes, because this is the highest value a 4 byte unsigned integer can store", I think there is a typo into "4,294,967,295 bytes", you meant "4,294,967,295 bit" or " 4,294,967,295 is the largest number"?

    • leo

      referring to the "std::size_t" operator which returns the size of an object as an unsigned integer of 4 bytes width which can carry a value up to (2^(4x8)) bytes , because the size is just a value, I recommend re-reading this part again whenever you feel like it.

  • Mohammad Ali

    Hi,

    It seems like this has been asked before, but there's still a thing I'm not quite clear on:

    - Integral data type (char, int, long, long long) don't specify a maximum for byte sizes, but do guarantee a minimum.
    - To cap maximum byte sizes, fixed-width variants of these data types were introduced, but they are optional for compilers to implement, causing cross-compatibility issues.
    - To deal with the portability issues, least and fast variants of integral data types were introduced that were mandatory for implementation by compilers adhering to the C++11 standard. However, these do not cap maximum byte size.

    What it seems like is that this simply brings us back to square one again. Fast / least variants only guarantee the minimum byte sizes but not the maximum, just like int, long, char etc. What additional benefits do these variants offer because it seems like there's nothing they do above and beyond what regular int, long, char types can already do?

    Thanks!

    • nascardriver

      For one, they're explicit. How wide is an `int` guaranteed to be? I certainly don't know. 16 bits maybe, I'd have to look it up. `std::int_fast16_t` on the other hand is 16 bit minimum no questions asked.
      Then, you have the choice between fast and least type, which you don't have when you use fundamental types directly.

      These types don't solve the issue of having a max-width type (Your point 2), they solve the obscurity of fundamental types.

  • yeokaiwei

    Dear Alex,

    Despite the above warning, "The above fixed-width integers should be avoided, as they may not be defined on all target architectures."

    uint32_t is still commonly used in APIs such as Vulkan and openGL.

    Why is this so?

    • nascardriver

      Some libraries need those types. `std::uint8_t` is commonly seen in libraries that store bytes, for example. I can imagine that OpenGL and Vulkan need `std::uint32_t` for fast transfer of colors to the GPU.

      • yeokaiwei

        Hi nascar,

        Every new programmer will see a conflict in advice versus industry.

        Is it a must to use for the graphics libraries to use std::uint32_t?

        Are there any suitable replacements?

        • nascardriver

          If you don't need exactly N bits, don't tell the compiler that you want exactly N bits. That's what the advice is saying.
          If you do need exactly N bits, use the fixed-width types.

          What "need" means depends on the situation. Many libraries could probably do without these types, but there'd likely be some downside. For example readability of the code, or performance.

          Suppose you want to cut open a bun and you have the choice between a butter knife and a chef's knife. Either would get the job done equally well, it doesn't matter which one you choose. You can bring a butter knife to more places (Airplanes, maybe), so that's the better choice.
          Though if you want the prepare a nice dinner, the chef's knife is what you should be grabbing, even if you can't bring it everywhere. You could use your butter knife to slice chicken and onions, but the result is going to be notably worse than if you had used the chef's knife.

  • Young Rob

    "The fixed-width integers have two downsides: First, they are optional and only exist if there are fundamental types matching their widths and following a certain binary representation."
    Is that they're optional for a compiler to include? That seems odd

  • tran duc phong

    [hello alex.
    1: I don't understand if size_t is a type like int, long .. (eg size_t x {}; declare x) or is it an operator to see the length of an object like sizeof?
    2: give me a specific example of size_t that I can understand what it is]

  • yeokaiwei

    1. Clarification
    What does "cstdint" stand for?

    Does <cstdint> mean <C standard integer>?

    • Ilan

      Ported libraries from C to C++ have their name changed. First, the suffix '.h' is removed, and the 'c' prefix is added.
      Example: <stdio.h> and <cstdio>, <string.h> and <cstring>.
      Hope it helped

      • yeokaiwei

        Thanks.

        The reason why clarification might be useful because the first time I read it, I read it as "constant dint".

        I got it on the 2nd read through but I realised that Google doesn't actually explain it either.

        Websites like, https://en.cppreference.com/w/cpp/header/cstdint, give similar answers to you.

        <cstdint> means "C Standard Integer Type Names".

        <cstddef> means "C Standard Definitions" for C Library Language Support

        <cstdarg> mean "C Standard Arguments" for Variable length function argument lists.

        <cstdlib> mean "C Standard Library" for Program Termination

  • Alex_101

    "For example, if std::size_t is 4 bytes wide, the largest object creatable on your system can’t be larger than 4,294,967,295 bytes, because this is the highest value a 4 byte unsigned integer can store." - Second last paragraph.

    Uhh, what do you mean by "larger than 4,294,967,295 bytes"? Did you mean "larger than the number 4,294,967,295? Because what you said doesn't make sense unless std::size_t is 4,294,967,295 bytes wide...

    • nascardriver

      It's correct as written.
      An object can be 4 bytes big.
      An object can be 100 bytes big.
      An object can be 10,000 bytes big.
      An object can be 4,294,967,295 bytes big.
      An object can't be 4,294,967,296 bytes big.

      • SomeNewGuy

        I don't fully understand this -- how does this relate to

        printing 4? I would expect this to be the size in bytes. I don't understand how you can deduct from this number that the largest object is at most 4,294,967,295 bytes wide.

        • nascardriver

          > I would expect this to be the size in bytes
          That's exactly what it does.
          `std::size_t` is 4 bytes wide on your system.
          4 bytes are 32 bits.
          32 bits can represent 2^32 = 4,294,967,295 different numbers.
          The largest number you can store in a `std::size_t` is 4,294,967,295.
          Therefore, no object can be larger than 4,294,967,295 bytes.

  • yeokaiwei

    On the author’s machine, this prints:

    4

    For the above section, may I suggest changing the tutorial answer from 4 to 4 bytes?

    Unless a new student knew what sizeof() was doing, a new student would easily confuse it as value = 4, instead of 4 bytes.

Leave a Comment

Put all code inside code tags: [code]your code here[/code]