Types and Values
How C++ thinks about types — categories, conversions, and why static typing is your friend.
In the previous chapter we said every variable has a type, and a type is a promise about memory and operations. Now let's zoom out and look at the C++ type system as a whole. The goal is not to memorize every category but to build a map of the territory so you recognize where every type fits.
Why types?
A type system answers three questions:
- What can I store? A
boolcan storetrueorfalse; astd::stringcan store any sequence of bytes. - What can I do with it? You can
<<astd::stringtostd::cout; you cannot<<a rawint[5]array. - What can be converted to what, and how dangerous is each conversion?
A static type system answers all three at compile time. Bugs caught by the compiler are bugs your users never see.
A map of C++ types
C++ types break down into a small family of categories:
Most of your day will be spent with int, double, bool,
std::string, std::vector, and your own classes — but it helps
to know the rest exists.
Integral types in detail
C++ integers come in several widths and two signedness flavors:
| Type | Bits | Range (signed) | Range (unsigned) |
|---|---|---|---|
char / signed char / unsigned char | 8 | -128..127 | 0..255 |
short | 16 | -32 768..32 767 | 0..65 535 |
int | 32 (typical) | ±2.1B | 0..4.29B |
long | 32 or 64 | varies | varies |
long long | 64 | ±9.2 quintillion | 0..1.8E19 |
std::size_t | 32 or 64 | n/a (unsigned) | size of any object |
std::size_t is the type returned by sizeof and used for sizes
and array indices. It is unsigned, which leads to a classic trap:
A good habit: use signed integer types for arithmetic and only
use std::size_t when you really need to interoperate with a size or
index from the standard library.
Floating-point types and their surprises
float and double represent real numbers, but only approximately.
The hardware uses IEEE-754 binary representation, which cannot
exactly store many "nice" decimal numbers.
0.1 + 0.2 is not exactly 0.3 in binary floating-point. Never
compare floats with ==; compare with a tolerance:
std::abs(a - b) < 1e-9. Use int (or long long) for money,
counts, and anything that needs to be exact.
Compound types preview
We will cover each of these properly in later chapters; for now you just need to recognize their shape.
int n; // a value
int* p; // a pointer to an int
int& r = n; // a reference to an int
int a[5]; // an array of 5 ints
struct Point { int x; int y; }; // a struct (aggregate type)
enum class Color { Red, Green, Blue }; // a scoped enumA pointer holds a memory address. A reference is an alias for another name. An array is a fixed-size block of consecutive values. A struct bundles several values into one. An enum class is a small set of named constants with its own type.
Type conversions
C++ has two big categories of conversions:
- Implicit (silent) conversions. The compiler inserts them for
you when there is a "standard" way to convert. Examples:
int -> double,int -> bool(zero is false, nonzero is true). - Explicit conversions. You write them yourself. Modern C++
prefers the named casts:
static_cast,const_cast,reinterpret_cast,dynamic_cast.
Use static_cast when you actually mean it. C-style casts
((int)3.9) work but are easy to grep-miss and easy to over-do
(they will silently strip const, perform reinterpretation, or do
both).
Truthiness
In conditions like if (x), C++ treats any non-zero value as
"true" and zero as "false." This includes pointers (a null pointer
is false; any non-null pointer is true).
For clarity in real code, prefer explicit comparisons (if (n != 0),
if (p != nullptr)).
Named constants and constexpr
For values that are truly constant and known at compile time, use
constexpr rather than const. constexpr guarantees the value
is computed at compile time and can be used wherever a compile-time
constant is needed (e.g. as an array size).
Prefer constexpr over preprocessor #define MAX 100 macros. The
constexpr version has a real type, respects scope, and the compiler
can reason about it.
Enumerations: a small set of named values
When a variable should hold one of a small fixed set of options,
use a enum class:
Why not just use int 0/1/2? Because then nothing prevents you
from writing l = 7; or comparing two unrelated enums. With
enum class, the type system catches misuse for free.
Type aliases
Long type names get repetitive. using declares a friendly alias:
using StringMap = std::map<std::string, std::string>;
StringMap config; // much shorter than spelling std::map<...> againAliases are just names — they don't create new types. But they make code dramatically more readable.
Reading C++ types right to left
When you see a complex type, read from the variable name outward.
const int* p; // p is a pointer to const int
int* const p; // p is a const pointer to int
const int* const p; // p is a const pointer to const intThe trick: the word const qualifies whatever is to its left
(or, if there is nothing to its left, whatever is to its right).
Practice this on small examples; the same pattern scales up.
Challenge: choose the right type
Implement double average(const std::vector<int>& xs) that returns the mean of xs. Pay attention to the types: xs.size() is std::size_t and the sum is int — you'll need a cast or careful arithmetic to get the right double result. The fixed input prints exactly 3 on success.
Test your understanding
Why is it dangerous to compare double values with ==?
The CPU does not support double equality at all.
The compiler always emits a warning, so the operation is forbidden.
Floating-point arithmetic is approximate; many decimal values cannot be represented exactly in binary, so the comparison may fail even for inputs that look mathematically equal.
It is dangerous only on 32-bit systems.
What's the safer modern alternative to a C-style cast like (int)3.9?
A reinterpret_cast.
A static_cast, e.g. static_cast<int>(3.9).
A const_cast.
A dynamic_cast.
What is the main reason to prefer enum class Color { ... } over plain integers for representing a small fixed set of options?
It is faster at runtime.
It uses less memory.
It gives the values a distinct type, so the compiler can prevent misuse like mixing colors with unrelated integers.
It is required by the C++ standard.
When should you reach for constexpr instead of const?
Never; const is always better.
Whenever you want a variable that might change later.
When the value is genuinely known at compile time and you may need it in a context that requires a compile-time constant (array sizes, template arguments, etc.).
Only inside template metaprogramming code.
Next: how to control the flow of a program — if, while, for,
switch, and how to think about them as decisions and loops.