Fixed Width Integers and size_t

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

The fundamental reasons why the size of integers in C and C++ is not fixed by default. The design philosophy of C, dating back to its early days, emphasized performance and efficiency, allowing compilers to choose the most appropriate size for integers based on the target computer architecture. In early days of C, when computers were slow and performance was of the utmost concern. Thus C opted to intentionally leave the size of an integer open so that the compiler implementers could pick a size for int that performs best on the target computer architecture.

Modern Era

In modern systems, Consider the int type. The minimum size for int is 2 bytes, but it's often 4 bytes on modern architectures. If you assume an int is 4 bytes because that's most likely, then your program will probably misbehave on architectures where int is actually 2 bytes (since you will be probably be storing values that require 4 bytes in a 2 byte variable, which will cause overflow or undefined behavior). If you assume an int is only 2 bytes to ensure maximum compatibility, then on systems where int is 4 bytes, you are wasting 2 bytes per integer and doubling your memory usage.

Fixed-width integers

To address the above issues, C99 defined a set of fixed-width integers (in the stdint.h header) that are guaranteed to be the same size on any architecture.

These are defined as follows:

NameTypeRange
std::int8_t1 byte signed-128 to 127
std::uint8_t1 byte unsigned0 to 255
std::int16_t2 byte signed-32768 to 32767
std::uint16_t2 byte unsigned0 to 65535
std::int32_t4 byte signed-2,147,483,648 to 2,147,483,547
std::uint32_t4 byte unsigned0 to 4,294,967,295
std::int64_t8 byte signed-9,223,372,036,854,775,808 to 9,223,372,036,854,775,807
std::uint64_t8 byte unsigned0 to 18,446,744,073,709,551,615

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

#include <cstdint> // for fixed-width integers
#include <iostream>

int main()
{
    std::int16_t i{5};
    std::cout << i << '\n';
    return 0;
}

Downsides of using fixed-width integers

Architecture Dependency:

  • The existence and availability of fixed-width integers are not guaranteed on all architectures. If the underlying system lacks fundamental types that match the specified widths and binary representations, the program may fail to compile. However, this concern is less relevant in practice for most modern architectures that commonly use 8/16/32/64-bit variables. It becomes more of an issue in niche scenarios involving exotic or specialized architectures where standard fixed-width integers might not be supported.

Performance Considerations:

  • The choice of fixed-width integers can impact performance, and the relationship between integer width and processing speed can be nuanced. For instance, a program using std::int32_t might be slower on a system that processes 64-bit integers more efficiently. The trade-off between the speed of processing and memory usage is crucial. While a CPU may handle a wider type more efficiently, the larger memory footprint of wider types could lead to increased cache misses and slower overall performance. Therefore, the choice between fixed-width integers and wider types should consider the specific requirements and constraints of the application, and it's often necessary to measure and profile actual performance to make informed decisions.

Fast and least integers

To help address the above downsides, C++ also defines two alternative set of integers that are guaranteed to be defined.

The fast type (std::int_fast#_t and std::uint_fast#_t) provide the fastest signed/unsigned 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. By fastest, we mean that integral type that can be processed most quickly by the CPU.

The least types (std::int_least#_t and std::uint_least#_t) provide the smallest signed/unsigned integer type with a width of at least # bits (where # = 8, 16, 32, or 64). For example, std::uint_least32_t will give you the smallest unsigned integer type that's at least 32 bits.

#include <cstdint> // for fast and least types
#include <iostream>

int main()
{
	std::cout << "least 8:  " << sizeof(std::int_least8_t) * 8 << " bits\n";
	std::cout << "least 16: " << sizeof(std::int_least16_t) * 8 << " bits\n";
	std::cout << "least 32: " << sizeof(std::int_least32_t) * 8 << " bits\n";
	std::cout << '\n';
	std::cout << "fast 8:  " << sizeof(std::int_fast8_t) * 8 << " bits\n";
	std::cout << "fast 16: " << sizeof(std::int_fast16_t) * 8 << " bits\n";
	std::cout << "fast 32: " << sizeof(std::int_fast32_t) * 8 << " bits\n";

	return 0;
}
// Output
least 8:  8 bits
least 16: 16 bits
least 32: 32 bits

fast 8:  8 bits
fast 16: 64 bits
fast 32: 64 bits

You can see that std::int_least16_t is 16 bits, whereas std::int_fast16_t is actually 64 bits. This is because on the author's machine, 64-bit integers are faster to process than 16-bit integers.

However, these fast and least integers have their own downsides: First, not many programmers actually use them, and a lack of familiarity can lead to errors. Second, the fast types can lead to memory wastage, as their actual size may be larger than indicated by their name. More seriously, because the size of the fast/least integers can vary, it's possible that your program may exhibit different behaviors where they resolve to different sizes. For example:

#include <cstdint>
#include <iostream>

int main()
{
    std::uint_fast16_t sometype { 0 };
    sometype = sometype - 1; // intentionally overflow to invoke wraparound behavior

    std::cout << sometype << '\n';

    return 0;
}
 
 // Output
 18446744073709551615

This code will produce different results depending on whether std::uint_fast_t is 16, 32, or 64 bits.

It's hard to know where your program might not function as expected until you have rigorously tested your program on such architectures.