C++ / FUNDAMENTAL TYPES AND VARIABLES
Integers and their sizes across platforms
Predict and verify integer widths on any C++ target: know the standard's minimum ranges, spot the LP64/LLP64 split, and pick <cstdint> types deliberately.
What you will learn
- Name the guaranteed minimums: char 8, short/int 16, long 32, long long 64 bits
- Explain why sizeof(long) is 8 on Linux but 4 on 64-bit Windows
- Choose between int32_t, size_t, uintptr_t and plain int by intent, not habit
- Lock a width assumption in place with static_assert so bad builds fail early
Understanding Integers and their sizes across platforms
C++ does not tell you how big an int is. The standard fixes minimum ranges - char holds at least 8 bits, short and int at least 16, long at least 32, long long at least 64 - plus the rule that each type in that chain is at least as wide as the one before it. Everything else is left to the implementation, and sizeof reports the answer in bytes, where a byte is CHAR_BIT bits rather than necessarily 8. So the claim that int is 4 bytes is a fact about your compiler and target, not about the language.
In practice compilers do not pick widths independently; they follow the data model of the platform's ABI. 64-bit Linux and macOS use LP64: int is 32 bits, long and pointers are 64. 64-bit Windows uses LLP64: int and long are both 32 bits, and only long long and pointers reach 64, because Microsoft kept long unchanged so that decades of 32-bit source kept compiling. That one difference is why long is the least portable integer type in the language, and why code assuming sizeof(long) == sizeof(void*) breaks the moment it is built with MSVC.
The fix is to name the property you actually depend on: exact-width aliases from <cstdint> like std::int32_t and std::uint64_t when the width is part of a file format, a packet, or a hardware register, and plain int when you just need a fast integer that is big enough. For sizes, memory indices, and pointer arithmetic reach for std::size_t, std::ptrdiff_t, and std::uintptr_t, which track the pointer width automatically instead of guessing at it. Where an assumption is unavoidable, spell it out as a static_assert so a platform with different widths fails to compile rather than misbehaving at runtime.
// Output below is from a 64-bit Linux x86-64 build (the LP64 data model).
<climits> // CHAR_BIT
<cstdint> // std::int32_t
<iostream>
<limits>
int main() {
// sizeof counts bytes; CHAR_BIT says how many bits one byte has.
std::cout << "CHAR_BIT = " << CHAR_BIT << '\n';
std::cout << "sizeof(short) = " << sizeof(short) << '\n';
std::cout << "sizeof(int) = " << sizeof(int) << '\n';
std::cout << "sizeof(long) = " << sizeof(long) << '\n';
std::cout << "sizeof(long long) = " << sizeof(long long) << '\n';
std::cout << "sizeof(void*) = " << sizeof(void*) << '\n';
std::cout << "sizeof(int32_t) = " << sizeof(std::int32_t) << '\n';
std::cout << "int range: " << std::numeric_limits<int>::min()
<< " .. " << std::numeric_limits<int>::max() << '\n';
std::cout << "long range: " << std::numeric_limits<long>::min()
<< " .. " << std::numeric_limits<long>::max() << '\n';
// Unlike the numbers above, these hold on every conforming compiler.
static_assert(sizeof(short) <= sizeof(int), "width chain");
static_assert(sizeof(int) <= sizeof(long), "width chain");
static_assert(std::numeric_limits<long>::max() >= 2147483647L, "long is at least 32 bits");
}
C++ guarantees minimum ranges and an ordering of the integer types, never exact sizes; the platform's data model picks the real widths, and long is where the models disagree.
Worked examples
Naming the data model from inside the program
Derives which ABI data model the build uses from the widths of long and void*.
<climits>
<cstddef>
<iostream>
const char* data_model() {
if (sizeof(int) == 4 && sizeof(long) == 8 && sizeof(void*) == 8) return "LP64";
if (sizeof(int) == 4 && sizeof(long) == 4 && sizeof(void*) == 8) return "LLP64";
if (sizeof(int) == 4 && sizeof(long) == 4 && sizeof(void*) == 4) return "ILP32";
return "unfamiliar";
}
int main() {
std::cout << "model: " << data_model() << '\n';
std::cout << "long bits: " << sizeof(long) * CHAR_BIT << '\n';
std::cout << "pointer bits: " << sizeof(void*) * CHAR_BIT << '\n';
std::cout << "size_t bits: " << sizeof(std::size_t) * CHAR_BIT << '\n';
}
Example explained
Line 1Every sizeof comparison in data_model is a compile-time constant, so the compiler already knows which branch is live for the target it is building.
Line 2long is the discriminator: 8 bytes means LP64, while 4 bytes together with 8-byte pointers is Win64's LLP64.
Line 3Multiplying by CHAR_BIT is how you get bits, because sizeof only ever reports byte counts.
Line 4size_t comes out at pointer width, not int width, since it must be able to express the size of the largest possible object.
Fixed-width aliases give the same answer everywhere
Shows that <cstdint> types have platform-independent width and range, and that int8_t is not a small int.
<cstdint>
<iostream>
<limits>
int main() {
std::cout << "int32_t bytes: " << sizeof(std::int32_t) << '\n';
std::cout << "int64_t max: " << std::numeric_limits<std::int64_t>::max() << '\n';
std::uint32_t ticks = std::numeric_limits<std::uint32_t>::max();
std::cout << "ticks: " << ticks << '\n';
++ticks; // unsigned: wraps modulo 2^32, well defined
std::cout << "after ++: " << ticks << '\n';
std::int8_t small = 65;
std::cout << "int8_t as-is: " << small << '\n';
std::cout << "int8_t as int: " << static_cast<int>(small) << '\n';
}
Example explained
Line 1std::int32_t is required to be exactly 32 bits, so on any 8-bit-byte platform sizeof reports 4 and the printed range is identical on Windows and Linux.
Line 2numeric_limits<std::uint32_t>::max() writes the ceiling of the type without hardcoding 4294967295 anywhere in your logic.
Line 3Incrementing an unsigned type at its maximum is defined to wrap to 0, which is why counters and hashes are usually declared unsigned.
Line 4std::int8_t is an alias for signed char, so the stream treats it as a character and prints A; the static_cast<int> is what makes it print as a number.
Important notes
CHAR_BIT is 8 on every mainstream desktop, server, and phone target, but the standard only requires at least 8; some DSPs use 16- or 32-bit chars, where sizeof(int) == 1 is perfectly legal.
Exact-width aliases are optional: std::int32_t exists only if the target has a type exactly that wide, whereas std::int_least32_t and std::int_fast32_t are always provided and are the portable fallback.
Common mistakes
Treating long as the 64-bit type because it is on Linux: with MSVC on x64 long is 32 bits, so long total = files * 1024L * 1024L * 1024L; overflows past 2 GB, and signed overflow is undefined behaviour rather than a wrapped-but-usable number.
Casting a pointer through long or int to store or log it: on Win64 the top 32 bits are discarded, and the value cast back is a pointer into nowhere; std::uintptr_t is the integer type that is allowed to round-trip a void*.
Writing a struct of int and long fields straight into a file or socket: a reader built with a different data model misplaces every field after the first long, so the data is not rejected, it is silently misread.
Try it yourself
Change, predict, then run
On an online compiler, print sizeof for short, int, long, long long, void* and std::size_t, then add static_assert(sizeof(long) == 4, "check") and see whether it compiles. State which data model that result proves, then rewrite the assertion so it passes on that target.
Open the C++ workspaceCheck your understanding
A program keeps a 64-bit file offset in a long. It works on Linux but produces wrong offsets past 2 GB when rebuilt for 64-bit Windows. What explains this?
- The C++ standard fixes long at 32 bits, so the Linux build was the non-conforming one
- Windows stores integers in the opposite byte order, which truncates values above 2 GB
- Win64 uses the LLP64 model, where long stays 32 bits even though pointers and long long are 64
- long is 64 bits in both builds, but MSVC clamps arithmetic results to the range of int
Show answer
The standard only sets a minimum range for long, so a 32-bit long on MSVC and a 64-bit long on GCC are both conforming; Win64 deliberately kept long at 32 bits for source compatibility with 32-bit Windows, so the offset overflows there. The first option is the tempting inversion of the rule - there is no exact size requirement for anyone to violate. Declaring the offset as std::int64_t makes both builds agree.