<< Home

Chapter 5: Expressions and Operators

C++ expressions are the gears that make computation move, and operators provide the teeth on those gears. This chapter tours operator categories, precedence, and associativity; the rules that decide when and how values convert; the meaning of lvalues and rvalues; and the main families of logical, bitwise, and arithmetic operators. You will also see how user defined conversions join the system, although careful design keeps them tame rather than surprising.

Operator categories and precedence

Operators fall into families such as arithmetic, logical, comparison, bitwise, member access, and assignment. Precedence tells you which operator binds more tightly when several appear in a single expression. Associativity tells you whether grouping proceeds from left to right or the other way. Knowing this prevents accidental ambiguities and keeps your expressions readable.

Major operator groups

Most operators behave predictably across categories. Arithmetic operators like + and - compute new numeric values. Comparison operators such as < and >= produce booleans. Logical operators combine truth values. Bitwise operators manipulate individual bits. Member access operators such as . and -> navigate object structure.

CategoryExamplesNotes
Arithmetic+, -, *, /, %Numeric operations with usual promotions.
Logical&&, ||, !Short circuit evaluation for && and ||.
Comparison<, >, <=, >=, ==, !=Return booleans and chain cleanly.
Bitwise&, |, ^, ~, <<, >>Operate on integer types at bit level.
Member access., ->, ::Navigate objects, pointers, and namespaces.
Assignment=, +=, -=, *=, /=, …Modify objects and return lvalues.

Precedence and grouping

Certain operators bind more tightly than others. Multiplication binds more tightly than addition. Member access binds more tightly than function calls, which bind more tightly than arithmetic. Parentheses override every rule and communicate intent clearly. Use them freely whenever code reads better with explicit grouping.

int r = a + b * c;    // multiplication first
int s = (a + b) * c;  // overridden grouping
auto p = &v[0];       // member access binds tightly
⚠️ Avoid clever expressions that rely on subtle precedence. Clarity helps both readers and compilers.

Conversions

C++ performs conversions in many expressions. Some are safe and quiet, such as integer promotions. Others can narrow or lose information. Explicit casts express your intent by telling the compiler exactly what type you want. Be clear about conversions because they shape the meaning of expressions.

Implicit conversions

Implicit conversions include arithmetic promotions, pointer adjustments, array to pointer conversions, and class defined conversions. Although implicit conversions make code flexible, they can hide precision loss. Narrowing is restricted in list initialization and should be avoided elsewhere unless you intentionally coerce values.

double d = 3;       // implicit int to double
int n = d;          // implicit double to int (truncation)
const char* p = s;  // array to pointer for string literal

Explicit casts

C++ provides static_cast, reinterpret_cast, const_cast, and in rare cases dynamic_cast. Prefer static_cast when converting between arithmetic types or related class types. Reserve reinterpret_cast for low level transformations such as bit reinterpretation. Use const_cast only to remove const from objects that were never originally const. Use dynamic_cast for checked downcasts in polymorphic hierarchies.

int n = static_cast<int>(3.14);  
auto base = static_cast<Base*>(derived_ptr); 
💡 Cast only when the target type is clear and stable. Most casts in mature C++ vanish once interfaces are shaped properly.

Value categories and move semantics

C++ expressions have value categories that describe how the resulting values behave. These categories decide whether you can bind references, whether you can move from an object, and how overload resolution selects functions. Although move semantics appear in depth later, this preview sets the stage.

Value categories

The central categories are lvalues, xvalues, and prvalues. An lvalue describes a stable location. An xvalue describes an expiring object that can be moved from. A prvalue describes a pure temporary value. C++11 merged several older forms into these three modern categories.

int n = 42;
n             // lvalue
std::move(n)  // xvalue
n + 1         // prvalue

Move readiness

Move semantics apply when an object is an xvalue or a prvalue that can be treated as an expiring object. A move constructor or move assignment operator can then steal resources rather than copy them. This speeds up code and reduces allocations. The preview here helps you spot where moves may occur once you read later chapters.

std::string make();
std::string s = make();  // move from temporary when possible
std::vector<int> v;
v.push_back(5);          // moves when resizing or reusing capacity
⚠️ Moving from an object leaves it valid but unspecified. Use it only in ways that do not assume old contents remain present.

Operator types

C++ offers rich operators for boolean logic, bit-level manipulation, and number crunching. Practice with these operators builds fluency and helps you avoid common pitfalls such as precedence confusion or sign extension during shifts.

Logical operators

Logical operators apply to boolean expressions. && and || perform short circuit evaluation, so the right-hand side executes only when needed. This style enables safe checks before dereferencing pointers or performing expensive computations.

if (ptr && *ptr > 0) { /* … */ }

Bitwise operators

Bitwise operators let you flip bits, mask values, or combine flags. They work on integer types and require attention to signedness because right shifts on signed values are implementation defined when negative. Use << and >> for shifts, & for masks, | for unions, and ^ for toggling bits.

unsigned mask = 0b1111;
unsigned value = 0b1010;
auto masked = value & mask;     // keep low four bits
auto flipped = value ^ mask;    // toggle low four bits

Arithmetic operators

Arithmetic operators follow familiar rules from mathematics. Addition, subtraction, multiplication, division, and remainder form expressions with predictable behavior except for division by zero or overflow. Unsigned overflow wraps, while signed overflow is undefined. Choose appropriate types for your domain.

auto sum = a + b;
auto diff = a - b;
auto prod = a * b;
auto quot = a / b;
auto rem = a % b;
💡 When shifting or masking, use unsigned types because their behavior is defined for all bit patterns.

User defined conversions

User defined conversions extend the conversion rules with constructors and conversion operators. Conversion constructors let objects form from other types. Conversion operators let objects turn into other types. Use both mechanisms with restraint because implicit conversions can create surprising paths during overload resolution.

Conversion constructors

A non explicit constructor with one parameter may act as an implicit conversion path. Consider marking such constructors explicit unless you truly want silent conversions. This keeps expressions stable and ensures that readers understand when a conversion is happening.

struct Meter {
  explicit Meter(double m) : value(m) {}
  double value;
};

Conversion operators

A conversion operator named operator T() allows objects to convert to another type. Keep such operators explicit in meaning and avoid creating broad or ambiguous paths. Modern code often prefers named functions rather than implicit conversions.

struct Angle {
  double radians;
  explicit operator double() const { return radians; }
};
⚠️ Avoid pairs of user defined conversions that form cycles because they may create ambiguous overloads or surprising implicit behavior.