Operators and Expressions

This section documents C++23 (ISO/IEC 14882:2024), as published by ISO/IEC JTC1/SC22/WG21 (wg21), verified against the freely available working draft N5046 (eel.is/c++draft) and cppreference.com.

This content was generated with the assistance of AI and should be verified against the working draft and cppreference.com before being relied on in production.

This section’s bibliography lists the reference material consulted while preparing these pages.

Operator Precedence and Associativity

C++ has around 40 operators across roughly 18 precedence levels (the full table is at cppreference, linked below); the ones that most often surprise newcomers:

int a = 2 + 3 * 4;          // 14, not 20 -- * binds tighter than +
bool b = 1 < 2 == true;     // ((1 < 2) == true) -- relational binds tighter than equality
int c = 1 << 2 + 1;         // 1 << (2 + 1) == 8 -- shift binds LOOSER than + (a frequent surprise)

When in doubt, add parentheses — they cost nothing at run time and remove any ambiguity for the reader.

Value Categories

Every C++ expression is exactly one of three value categories, which govern whether it can be moved from and whether its address can be taken:

  • lvalue — has identity, cannot (in general) be moved from implicitly: a named variable, *ptr, arr[i].

  • prvalue ("pure rvalue") — has no identity, is the initializer of the object it produces: a literal, the result of a + b, a temporary from a by-value return.

  • xvalue ("eXpiring value") — has identity and may be moved from: the result of std::move(x), or a function returning T&&.

int x = 5;
int& lref = x;              // x is an lvalue -- binds to int&
int&& rref = 5;              // 5 is a prvalue -- binds to int&&
int&& rref2 = std::move(x);  // std::move(x) is an xvalue -- also binds to int&&

lvalues and xvalues are jointly called glvalues ("generalized lvalues"); prvalues and xvalues are jointly rvalues. See Move Semantics and Value Categories for how this drives overload resolution between copy and move.

Sequencing

Since C++17, most compound expressions with two operands have a guaranteed evaluation order, which fixed several pre-C++17 undefined-behavior traps. The order depends on the operator: in a simple assignment the right operand is sequenced before the left one, while <</>> chains and function-call argument-to-callee sequencing run left-to-right:

#include <vector>

int main() {
    std::vector<int> v = {1, 2, 3};
    int i = 0;
    v[i] = i++;             // well-defined since C++17: the right operand (i++) is sequenced before the left
                            // one, so i++ yields 0 and leaves i == 1 -- the write lands on v[1], not v[0]
    return v[1];             // deterministically 0; v[0] is untouched and still 1
}

Function argument evaluation order relative to other arguments is still unspecified, though — f(g(), h()) may call h() before g(); do not rely on the order.

Integer Promotions

Before most arithmetic/comparison operators run, small integer types (bool, char, short, and unscoped enums) are promoted to int (or unsigned int if int cannot hold every value):

char a = 100, b = 100;
int result = a + b;         // both promoted to int first: 200, not a char overflow
static_assert(sizeof(a + b) == sizeof(int));

<⇒: Three-Way Comparison

The spaceship operator (C++20) returns an ordering category and lets the compiler synthesize the other five relational operators from one function:

#include <compare>

struct Version {
    int major, minor, patch;
    auto operator<=>(const Version&) const = default;   // defaults to lexicographic member-wise comparison
};

static_assert(Version{1, 2, 0} < Version{1, 3, 0});
static_assert(Version{2, 0, 0} > Version{1, 9, 9});
static_assert(Version{1, 0, 0} == Version{1, 0, 0});

See Operator Overloading and Conversions for hand-written (non-defaulted) <⇒ and the three ordering categories it can return.

Safe Integer Comparison: std::cmp_less and Friends

Comparing a signed and an unsigned integer directly is a classic bug source: the signed value converts to unsigned first, so -1 < 1u is false. C++20’s <utility> comparison functions compare the mathematical values instead:

#include <utility>
#include <cassert>

int main() {
    int signedValue = -1;
    unsigned unsignedValue = 1;

    assert((signedValue < unsignedValue) == false);          // the classic pitfall: -1 wraps to a huge unsigned
    assert(std::cmp_less(signedValue, unsignedValue) == true); // std::cmp_less compares the real values: -1 < 1
    return 0;
}

std::cmp_equal, std::cmp_less, std::cmp_greater, std::cmp_less_equal, std::cmp_greater_equal, and std::in_range<T>(value) round out the set.

Bit Manipulation

#include <bit>
#include <cstdint>
#include <cassert>

int main() {
    uint32_t x = 0b0000'0000'0000'0000'0000'0000'0010'1100;
    assert(std::popcount(x) == 3);          // number of set bits (C++20)
    assert(std::has_single_bit(8u));         // is x a power of two?
    assert(std::bit_width(8u) == 4);          // bits needed to represent x
    uint32_t rotated = std::rotl(x, 4);       // rotate left (C++20)
    (void)rotated;

    uint32_t y = 5;
    y |= (1u << 3);      // set bit 3
    y &= ~(1u << 0);     // clear bit 0
    y ^= (1u << 1);      // toggle bit 1
    (void)y;
    return 0;
}

See Also

  • C: Operators and Expressions — the same operator set and precedence, but C still leaves arr[i] = i++ undefined where C++17 defines the order.