Chapter 03 of 24 | C++

Values, types, and variables

A variable is a named object with a type and a lifetime. Its type controls which values it can represent and which operations are valid. C++ checks these rules during compilation, but you still need to choose types that match your domain and avoid unintended conversions.

Common value types

int count = 12;
double temperature = 21.5;
bool is_ready = true;
char grade = 'A';
std::string name = "Ada";

Include <string> to use std::string. Use int for ordinary whole-number counts, floating-point types for approximate real-number measurements, and bool for true-or-false state. A floating-point value is not an exact decimal, so it is usually a poor choice for exact currency arithmetic.

Signed and unsigned integer types have different ranges and conversion rules. Prefer a signed type unless a nonnegative range is genuinely required; mixing signed and unsigned values can produce surprising comparisons. Use std::size_t when working with sizes returned by standard containers, while keeping conversion boundaries explicit.

Initialization and deduction

int attempts{3};       // direct, brace initialization
auto ratio = 0.25;     // deduced as double
const int limit{10};   // cannot be reassigned

Braces make initialization visible and reject many narrowing conversions. For example, initializing an integer from a fractional value with braces is diagnosed instead of silently discarding the fraction. auto asks the compiler to deduce a type from the initializer; it does not make the value dynamically typed. Use it when the initializer makes the type clear, not to hide important design information.

const prevents modification through that name after initialization. For values that should be compile-time constants, consider constexpr when the initializer is a constant expression.

Conversions deserve attention

int whole{7};
double precise = whole;                 // widening conversion
int rounded = static_cast<int>(3.9);    // becomes 3, not 4

Conversions can lose information or change signedness. Prefer an explicit static_cast when a conversion is intentional, and check ranges before converting external input.

Choosing a type is part of the design

Start by asking what the value means and what operations the program needs. An item count is usually an integer; an on/off setting is a Boolean; a measured distance may need a floating-point type. If the set of valid states is small and known, an enumeration can be clearer than an integer. A type is a small form of documentation that also lets the compiler reject many invalid operations.

Built-in integer types do not promise that every platform uses the same width for every type. The standard guarantees minimum ranges, not that int is always 32 bits. If a data format or hardware interface requires a specific width, use the fixed-width types in <cstdint> when the implementation provides them, and consider the portability of the interface. For collection sizes, use the type returned by the collection rather than assuming it is an int.

Initialization is not assignment

Initialization creates an object with its starting value; assignment changes an object that already exists. This distinction matters because a local built-in variable declared without an initializer does not automatically contain zero. Reading such an indeterminate value is unsafe. Prefer declarations that show the intended initial value:

int tries{};                 // value-initialized to zero
std::string title{"Notes"};  // initialized with text
const int max_tries{5};      // must be initialized

Brace initialization is a strong default for direct initialization because it exposes narrowing. There are contexts where braces select a special initializer-list constructor, so understand the type being constructed if an overload behaves unexpectedly. For everyday simple values, int count{3}; is clear and safe.

Const, constexpr, and mutability

Use const to express that a value should not be changed after it is established. This reduces the number of possible states you need to consider while reading a function. Use constexpr when a value can be computed during compilation and is intended to be usable in constant expressions. A constexpr variable is also const, but not every const value is necessarily known at compile time.

Give each variable a narrow purpose and a useful name. Names such as value1 communicate little; subtotal_cents tells the reader what the number represents and its unit. Units in names are a practical safeguard against mixing values such as seconds and milliseconds.

Floating-point expectations

Binary floating-point stores many decimal fractions approximately. As a result, a calculation such as 0.1 + 0.2 may not be represented as exactly 0.3. For measurements and scientific calculations, this is expected; compare with a tolerance appropriate to the problem rather than assuming exact decimal equality. For currency, store a whole-number count of the smallest unit or use a decimal arithmetic library when the requirements demand it.

Practice

Key idea: choose a type that communicates meaning, initialize every object, and make potentially lossy conversions deliberate.

Integer representation, ranges, and conversion rules

Integer types have ranks and ranges that participate in the integral promotions and the usual arithmetic conversions. An expression involving two small integer types may be evaluated after both operands are promoted to int. When signed and unsigned operands meet, the common type can be unsigned, which can make a comparison with a negative signed value surprising. Do not repair such warnings with a cast until you understand which mathematical range the operation is intended to represent.

<limits> provides std::numeric_limits<T> for querying properties such as minimum and maximum values, precision, and whether a type is signed. Fixed-width aliases from <cstdint> are useful when a protocol or binary layout specifies a width, but availability and the type behind an alias depend on whether the platform provides that exact-width type. Choose domain types first, then make boundary conversions explicit.

Initialization, object lifetime, and invariants

A declaration begins an object's lifetime when the language's initialization rules are satisfied. Initialization syntax can select constructors and affect overload resolution, not merely assign a value after creation. Brace initialization rejects many narrowing conversions, but it can also prefer an initializer-list constructor. For a container, for example, braces may mean “construct from these elements” rather than “construct with this size.” When syntax selects an unexpected overload, inspect the candidate constructors and use an explicit form that communicates the intended operation.

Types can encode domain constraints. A raw int can represent negative and positive values even when a value must be positive. A small wrapper class can validate construction and provide only valid operations. This is a gradual move from “the variable stores a number” to “the type represents a valid concept.” Such modeling reduces repeated defensive checks and makes invalid states harder to create.

Floating-point model and numerical error

Most implementations use binary floating-point formats described by IEEE 754, but the C++ language and implementation details should still be checked for a target. Rounding can occur at each operation; reassociating (a + b) + c into a + (b + c) may change a result. NaN is unequal to itself and can make ordinary ordered comparisons false. Positive and negative infinity have special behavior. These values matter in scientific code and can break naive sorting or validation logic.

Numerical analysis asks how input uncertainty and rounding propagate through an algorithm. An absolute tolerance suits values near zero, while a relative tolerance scales with magnitude; some applications need both. For stable algorithms, changing the mathematical formulation can be more effective than increasing precision. When correctness depends on exact decimal values, choose an exact representation rather than attempting to patch binary rounding with an arbitrary epsilon.

Advanced exercise

  1. Use std::numeric_limits to report the range and precision of several types on your compiler.
  2. Find a signed/unsigned comparison that triggers a warning. Explain the conversions and rewrite the program so the valid domain is explicit.
  3. Compare two summation orders over values with very different magnitudes and discuss numerical stability.
0% read