A type defines the values an object can hold and the operations available on it. C++ checks types before execution, but it also allows implicit conversions. A program can compile while silently discarding a fractional part or comparing signed and unsigned values in a surprising way. Learning those boundaries matters more than memorizing every numeric spelling.
Start by initializing every variable. An ordinary local declaration such as int count; does not initialize its value; reading that indeterminate int in C++20 has undefined behavior. int count{}; initializes it to zero. Braced initialization also rejects many narrowing conversions, such as initializing an int directly from 3.5.
Choose values deliberately
| Type or form | Typical role | Important boundary |
|---|---|---|
bool | True or false | Prefer explicit conditions |
int | Small whole-number calculations | Width is implementation-defined |
long long | A wider signed integer | At least 64 bits, not necessarily exactly 64 |
double | Approximate real-number calculations | Many decimal fractions are inexact |
char | A narrow character code unit | Signedness varies by implementation |
std::size_t | Sizes and nonnegative indexes | Unsigned; subtraction can wrap |
const prevents changing an object through that name after initialization. constexpr declares a value usable in constant expressions when its initialization meets the requirements. Neither is a substitute for choosing a sensible unit: timeout_seconds communicates more than timeout.
auto asks the compiler to deduce a type from the initializer. auto count = 4; deduces int; auto ratio = 4.0; deduces double. It does not create a dynamically typed variable. Use it when the initializer makes the type clear, and write a type when the choice itself is important.
A checked arithmetic example
Save as types.cpp:
#include <iostream>
#include <limits>
bool can_add(long long left, long long right) {
if (right > 0) {
return left <= std::numeric_limits<long long>::max() - right;
}
if (right < 0) {
return left >= std::numeric_limits<long long>::min() - right;
}
return true;
}
int main() {
constexpr long long price_cents{1250};
constexpr long long shipping_cents{250};
if (!can_add(price_cents, shipping_cents)) {
std::cerr << "Total is out of range\n";
return 1;
}
const auto total = price_cents + shipping_cents;
const int completed{7};
const int planned{10};
const double fraction = static_cast<double>(completed) / planned;
std::cout << "Total cents: " << total << '\n';
std::cout << "Completed: " << fraction << '\n';
std::cout << std::boolalpha
<< "Adding one to max is safe: "
<< can_add(std::numeric_limits<long long>::max(), 1) << '\n';
}
clang++ -std=c++20 -Wall -Wextra -Wpedantic -g types.cpp -o types
./types
Expected output:
Total cents: 1500
Completed: 0.7
Adding one to max is safe: false
The guard tests whether addition is representable before performing it. Signed integer overflow is undefined behavior; detecting it by first calculating the overflowing result is already too late. The two branches avoid overflowing their own checks, including when right is the minimum representable value.
Integer division truncates toward zero: 7 / 10 produces zero. Converting one operand before division selects floating-point arithmetic. Converting the already computed integer result afterward would preserve the mistake. Division by zero is invalid for integer operands; the minimum signed value divided by negative one is another unrepresentable case.
Conversions and representation pitfalls
Unsigned arithmetic wraps modulo one more than the largest representable value. That is defined behavior, but an unsigned countdown can wrap instead of stopping. When comparing a potentially negative integer with a container's size, first reject negatives or use C++20's std::cmp_less from <utility>. A cast merely requests a conversion; it does not prove that the original value fits.
Do not assume sizeof(int) is four or that a byte always has eight bits. Use std::numeric_limits to inspect ranges. Fixed-width types such as std::int64_t are provided when an implementation supports the corresponding exact-width type; a portable algorithm should state its actual range requirements.
Represent money in integer minor units when the application's currency and rounding rules permit it. Floating-point values are useful for measurements and ratios, but equality tests on independently computed approximate values need a problem-specific tolerance. No universal epsilon solves every comparison.
Practice and solution guidance
- Test
can_addwith zero, both limits, and two negative inputs. Guidance: test representable boundary results as well as rejected overflows; never evaluate the unsafe sum. - Calculate the average of three scores. Guidance: avoid integer division and ensure the sum itself cannot overflow before converting it.
- Design a duration variable and its accepted range. Guidance: document units, reject invalid input before conversion, and distinguish a constant value from a runtime value that simply remains unchanged.
Primary references
- WG21 C++20 draft: fundamental types and object rules.
- WG21 C++20 draft: arithmetic expressions and conversions.
- Clang UndefinedBehaviorSanitizer documents runtime checks for errors including signed overflow.

