C++ Part I · Module 3 · Types, Variables, and Initialization
Fixed-Width Integer Types for Hardware Data
Chapter 3.2 left a problem unresolved. The size of int and long varies between platforms, and that is fine for a loop counter whose exact width nobody cares about. It is not fine for a value that came out of a 32-bit register, because that value is thirty-two bits on every machine that will ever read it. The width is part of what the value is, not a property of the computer you happen to be compiling on. C++ gives you types that say the width out loud, and for an engineer working anywhere near hardware they are the default rather than a specialisation. This chapter covers which types to use and when, and then spends most of its length on the part that is actually difficult: keeping apart three different widths that beginners merge into one — the width a specification states, the width you store the value in, and the width the value takes on the moment you do arithmetic with it.
Foundation17 min readC++Part IFundamentalsHardware Data
Part I · Module 3 · Chapter 3.4 · Fixed-Width Integer Types for Hardware Data
1. When the Width Is Part of the Specification
Two variables, two different kinds of decision:
int loop_index; // how wide? nobody cares
?? status_register; // 32 bits, because the register is 32 bitsFor loop_index, the width is a convenience. Any type big enough works, and int is the natural choice because it is the type the machine is comfortable with.
For status_register, the width is part of the value's identity. The hardware designer specified a 32-bit register. The documentation says 32 bits. A testbench reading it gets 32 bits. If your variable is a different width, you have changed the data.
That distinction is the whole chapter:
When the width is a convenience, use
int. When the width is part of a specification, use a type that states the width.
2. The Types
They live in <cstdint>:
#include <cstdint>
std::uint32_t status_register; // exactly 32 bits, unsigned
std::int16_t timing_offset; // exactly 16 bits, signedThe naming is regular, so there is nothing to memorise:
| Type | Width | Signedness |
|---|---|---|
std::uint8_t | 8 bits | unsigned |
std::uint16_t | 16 bits | unsigned |
std::uint32_t | 32 bits | unsigned |
std::uint64_t | 64 bits | unsigned |
std::int8_t … std::int64_t | same widths | signed |
Measured on the machine used for this chapter:
sizeof(std::uint8_t) = 1
sizeof(std::uint16_t) = 2
sizeof(std::uint32_t) = 4
sizeof(std::uint64_t) = 8
sizeof(std::int32_t) = 4Unlike the table in Chapter 3.2, these numbers are not a property of this platform. std::uint32_t is 32 bits wherever it exists, by definition. That is the point of the type.
A note on spelling: the header also makes the names available without std::. This curriculum writes std::uint32_t, consistent with the reasoning in Chapter 2.2 §3.
3. A First Example
#include <cstdint>
#include <iostream>
int main()
{
std::uint32_t register_value = 0x0000'001Fu;
std::uint32_t address = 0x4000'1004u;
std::cout << "register_value = " << register_value << '\n';
std::cout << "address = " << address << '\n';
return 0;
}Expected output:
register_value = 31
address = 1073745924Three things in that code are worth naming.
The 0x prefix writes a number in hexadecimal. 0x1F is 31. Hexadecimal is the normal way to write hardware values because each digit is exactly four bits, so the digits line up with the bit positions the documentation uses.
The ' separators — 0x4000'1004 — are digit separators, purely for readability. The compiler ignores them. They are a C++14 feature and work throughout this curriculum's C++17 baseline.
The u suffix says the literal is unsigned, matching the type it is being stored in.
The output is in decimal, which is not how anyone wants to read an address. Printing in hexadecimal is a formatting question and Chapter 23.2 owns it. It is genuinely useful here, and it is worth waiting for the chapter that explains the whole formatting system.
4. What the Standard Actually Promises
This is where an authoritative tutorial has to be careful, because the common summary is slightly wrong.
The exact-width types are optional. std::uint32_t is required to exist only if the implementation has an integer type that is exactly 32 bits wide with no padding bits. On a machine with no such type, the name is simply not provided.
In practice:
- On every mainstream toolchain you are likely to meet — desktop, server, and the compilers shipped with simulators and embedded targets — all the common exact-width types exist.
- The standard does not require them, so "C++ guarantees
std::uint32_t" is not a true sentence.
If you ever need a type that is guaranteed to exist, the header provides two other families: std::uint_least32_t (at least 32 bits, smallest such type) and std::uint_fast32_t (at least 32 bits, fastest such type). Those are always provided. You will rarely need them, and this curriculum will not use them further — they are named here so that the honest version of the rule is available to you.
5. Three Different Widths
This is the section that matters most, and the one where beginners go wrong.
A 5-bit field in a register prompts the question: "which type holds 5 bits?" The answer is that no C++ type is 5 bits wide, and that is not a limitation — it is a sign that the question merges three separate ideas.
The logical field width is what the specification says. The field is 5 bits. That is a fact about the hardware and it never changes.
The storage width is the type you keep it in. The smallest addressable type is 8 bits, so a 5-bit field is stored in a std::uint8_t, with three bits left over. Storage comes in whole types; fields do not.
The arithmetic width is what the value becomes when you compute with it. C++ does not do arithmetic in 5 bits or even in 8 — small types are widened before the operation. Chapter 3.3 §4 already needed a static_cast for exactly this reason. Module 4 owns the rule.
So for a 5-bit field:
std::uint8_t clock_divider; // a 5-bit field, stored in 8 bitsThe type does not enforce the 5-bit limit. Nothing stops you putting 200 in it. Keeping the value within 5 bits is your responsibility, and the technique for extracting a sub-field from a larger word, and putting one back, is masking — which needs the bitwise operators that Chapter 4.8 teaches. This chapter deliberately shows none of them.
6. The Trap: std::uint8_t Prints as a Character
Every engineer meets this once. Better to meet it here.
#include <cstdint>
#include <iostream>
int main()
{
std::uint8_t error_count = 65;
std::cout << "direct: " << error_count << '\n';
std::cout << "cast: " << static_cast<unsigned>(error_count) << '\n';
return 0;
}Expected output:
direct: A
cast: 65It printed the letter A.
Nothing is broken. On essentially every mainstream implementation, std::uint8_t is a name for unsigned char — a character type. When you send a character type to a stream, the stream does what you would want for a character and prints the character whose code that is. The code for A is 65.
This is Chapter 3.1 §5 again: the same bits, interpreted according to the type. The type here is a character type, so the stream interpreted it as text.
The fix is to convert to a type the stream will treat as a number:
std::cout << static_cast<unsigned>(error_count) << '\n';Worth knowing beyond the printing:
- The same applies to reading a
std::uint8_tfrom a stream — it reads a character, not a number. std::int8_tbehaves the same way, assigned char.- This is the one place where the exact-width types have a genuine wrinkle, and it comes from
uint8_tbeing a character type rather than from anything about fixed widths.
7. Where This Belongs in Engineering Code
Concrete guidance, since this is the chapter that shapes how you write everything hardware-adjacent.
Use an exact-width type when:
| Situation | Type |
|---|---|
| A register value | std::uint32_t (or the register's actual width) |
| A memory address held as a number | std::uint32_t / std::uint64_t, matching the address map |
| A protocol field being parsed or assembled | the field's specified width |
| A byte of payload data | std::uint8_t |
| A value crossing a language or tool boundary | the width both sides agreed on |
Use int or another ordinary type when:
| Situation | Type |
|---|---|
| A loop counter | int |
| A count of things in your own program | int |
| An intermediate arithmetic result | wide enough not to overflow |
| A size or index into a container | std::size_t, which Module 9 covers |
The signedness question from Chapter 3.3 still applies on top of this. Hardware values are usually unsigned because they are bit patterns rather than quantities. A value that genuinely can be negative — a timing offset, a signed sample from a converter — uses the signed form: std::int16_t, std::int32_t.
8. Mistakes Worth Making Once
Using int for a register value. It works until the code is built somewhere int is a different width, or until someone assumes 32 bits and gets 16.
Printing a std::uint8_t and seeing a letter. It is a character type. Convert before printing.
Picking the smallest type that fits, always. Right for storage and layout; frequently wrong for arithmetic. A 12-bit sample being averaged wants room.
Expecting the type to enforce a sub-field's width. A std::uint8_t holding a 5-bit field will happily hold 200. Masking enforces the range, and that is Chapter 4.8.
Assuming arithmetic happens at the storage width. It does not; small types are widened first. Module 4 has the rule, and Chapter 3.3 §4 already needed a cast because of it.
Saying "C++ guarantees std::uint32_t exists". It is provided when the implementation has such a type. True everywhere you will work; not a language guarantee.
Forgetting #include <cstdint>. The names are not built in. The error is "unknown type name", which Chapter 2.7 §4 classified as a compilation error pointing at the line that used it.
9. Interview Relevance
This chapter carries a Critical weighting because the question behind it is a reliable discriminator.
"Why would you use std::uint32_t instead of int?" is asking whether you understand that width can be part of a value's specification rather than a property of the machine. A candidate who answers "it is more precise" has said nothing. A candidate who says "because a register is 32 bits on every platform and int is not, and this code gets built by toolchains I do not choose" has given the engineering reason.
The strong follow-up answer volunteers the three widths from §5 — field, storage, arithmetic — because that distinction is what separates someone who has used these types from someone who has read about them.
10. What You Should Be Able to Say Now
- When the width is a convenience, use
int. When the width is part of a specification, use a type that states it. <cstdint>providesstd::uint8_tthroughstd::uint64_tand the signed equivalents.std::uint32_tis 32 bits wherever it exists, by definition.- Those exact-width types are optional in the standard — provided when the implementation has such a type. Every mainstream toolchain provides them; "C++ guarantees them" is not accurate.
std::uint_least32_tandstd::uint_fast32_tare always provided, if you ever need a guarantee.0xwrites hexadecimal,'is a readability separator the compiler ignores, andumarks an unsigned literal.- Three different widths must be kept apart: the logical field width from the specification, the storage width of the type, and the arithmetic width the value is widened to in expressions.
- No C++ type is 5 bits wide. A 5-bit field is stored in a
std::uint8_t, and the type does not enforce the range — masking does, which is Chapter 4.8. - Do not always pick the smallest type that fits. Exact widths are for layout and specification; arithmetic wants room.
std::uint8_tis a character type on mainstream implementations, so it prints as a letter. Convert tounsignedbefore printing. The same applies when reading.- Hardware values are usually unsigned because they are bit patterns; use the signed forms for genuinely signed quantities.
- The reason this is a default is portability across toolchains you do not choose — Chapter 3.2 §5's
longhazard made real.
11. Exercises
Exercise 1 — choose the type. Give a type and one line of justification for each: (a) a loop running 100 times; (b) a 32-bit status register; (c) a byte of packet payload; (d) a signed timing offset specified as 16 bits; (e) the running total of a thousand 16-bit lengths.
Exercise 2 — predict the output. Without running it, say what this prints and why:
std::uint8_t id = 66;
std::cout << id << '\n';Then give the one-line fix.
Exercise 3 — name the three widths. A specification describes a 3-bit mode field inside a 32-bit control register. Identify the logical field width, a sensible storage width, and say what happens to the arithmetic width when you add 1 to it. Name the chapter that owns the last one.
Exercise 4 — argue against the smallest type. A colleague stores 12-bit ADC samples in std::uint16_t and then accumulates a thousand of them in another std::uint16_t. Identify the problem, say which of §5's three widths they confused, and give the fix.
Exercise 5 — the portability case. Explain, in two sentences and with reference to a specific platform difference from Chapter 3.2, why storing a 64-bit cycle count in a long is a defect rather than a style choice.
Exercise 6 — make the claim precisely. Rewrite this sentence so it is accurate: "C++ guarantees that std::uint32_t is available on every implementation."
Exercise 7 — what the type does not do. You store a 5-bit field in a std::uint8_t and assign it 200. Does it compile? Does it run? What has gone wrong, and which mechanism would have prevented it?
12. Summary
- Width as a convenience →
int. Width as a specification → an exact-width type. <cstdint>givesstd::uint8_t…std::uint64_tand the signed forms; each is exactly its stated width wherever it exists.- They are optional in the standard — universally available on the toolchains this field uses, but "C++ guarantees them" is not true.
uint_least/uint_fastalways exist. - Three widths, kept apart: the field width from the specification, the storage width of the type, the arithmetic width in an expression.
- No type is 5 bits wide. A 5-bit field lives in a
std::uint8_tand the type enforces nothing; masking does, in Chapter 4.8. - Smallest-that-fits is a layout rule, not an arithmetic rule.
std::uint8_tprints as a character — measured, 65 printed asA. Convert tounsignedfirst.- The real argument is portability across toolchains you do not choose, which is where
longbeing 4 bytes on one major platform stops being trivia.
Integers are only half of the fundamental numeric types. The other half exists because a great deal of engineering work involves quantities that are not whole numbers — a supply voltage, a measured delay, a throughput figure, a statistical result. Those need a different representation, and that representation has a property worth understanding before you rely on it: it stores numbers in binary, and most of the decimal fractions people write down cannot be represented in binary exactly. Chapter 3.5 shows what is actually stored when you write 0.1, why two numbers that should be equal are not, and why that is a predictable consequence rather than inaccuracy.
Standards & specifications
- Governing standard
- ISO/IEC 14882 (the C++ language standard)(opens ISO/IEC (JTC 1/SC 22/WG 21) in a new tab)
Defines the C++ language and its standard library — the type system and templates, the object model and object lifetime, initialisation and overload resolution, the memory model and the containers, algorithms and utilities the library provides. Like C's standard it is a FAMILY of editions (C++98/03, C++11, C++14, C++17, C++20, C++23) and the editions are NOT interchangeable: a feature this curriculum relies on may be unavailable, differently specified or merely proposed in another edition, so each lesson names the edition its claim depends on. It does not define any foreign-function interface to a hardware description language, nor what a compiler actually emits, nor platform behaviour on real silicon — those are the implementation's and the device's contracts. Working drafts published by WG21 are freely available and are what most engineers read day to day; a draft is not the published standard.
This page also covers RTL structure, verification approach and debugging technique. Those are engineering practice built on the standard, not requirements the standard itself imposes.
Where this fits
Part of the C++ Programming curriculum.
