Polyglot
Languages C++ operators
C++ § operators

Operators

C++ inherits C’s operator set and adds the scope-resolution operator ::, the member-access-through-pointer operators .* and ->*, the three-way comparison operator <=> (C++20), the casts static_cast/dynamic_cast/reinterpret_cast/const_cast, the storage-management operators new and delete, the throw and typeid operators, and the coroutine operators co_await, co_yield, co_return. Beyond the syntax, C++ admits operator overloading: most operators may be redefined for user-defined types, with conventions about which to overload and how. The sum of these additions makes C++‘s operator surface substantially larger than C’s, and the conventions for overloading them constitute one of the core skills of writing idiomatic C++.

Inherited operators

The arithmetic, comparison, logical, bitwise, and assignment operators are essentially those of C and have the same precedence and associativity. The full table appears at the end of this page.

int  a = 7,  b = 2;
int  q = a / b;        // 3
int  r = a % b;        // 1
bool t = (a > b) && (b != 0);

int   x = 0b1100;
int   y = 0b1010;
int   z = x & y;       // 0b1000

The treatment in C operators covers the inherited surface; the additions specific to C++ are discussed below.

C++ additions

The scope-resolution operator ::

:: accesses members of namespaces, of classes, and of enumeration types:

std::cout << "hello\n";        // member of namespace std
Widget::create();              // static member of class Widget
Direction::North;              // enumerator of an enum class
::global_function();           // explicit reference to the global namespace

The operator binds tighter than nearly everything else; it is part of the qualified name grammar rather than the expression grammar.

Member-access through pointer-to-member

.* and ->* apply a pointer-to-member to an object or to a pointer-to-object:

struct Point { double x, y; };

double Point::*coord = &Point::x;
Point  p{3.0, 4.0};
Point *pp = &p;

double a = p.*coord;        // 3.0
double b = pp->*coord;      // 3.0

The construction is rare in routine code; it is principally useful in generic code that takes a member pointer as a parameter (std::bind, std::invoke).

The three-way comparison <=>

C++20 introduced a three-way comparison operator that returns an ordering relation:

#include <compare>

int  a = 1, b = 2;
auto cmp = a <=> b;        // std::strong_ordering::less

The result type is one of std::strong_ordering, std::weak_ordering, std::partial_ordering depending on the type. The mechanism is principally useful for defaulted comparison, in which <=> is auto-generated for a class and the other comparison operators (==, !=, <, <=, >, >=) are derived from it:

struct Version {
    int major, minor, patch;
    auto operator<=>(const Version&) const = default;
};

Version v1{1, 0, 0}, v2{1, 2, 0};
bool b1 = (v1 < v2);    // true
bool b2 = (v1 == v2);   // false

A single defaulted <=> declaration generates the full set of comparison operators with the correct semantics.

Casts

C++ provides four named cast expressions instead of (or alongside) C’s parenthesised cast:

int n = static_cast<int>(3.14);             // arithmetic conversion
Base *b = dynamic_cast<Base*>(d_ptr);        // polymorphic downcast
auto addr = reinterpret_cast<uintptr_t>(p);  // pointer-as-integer
auto r    = const_cast<int*>(&ci);           // remove const-ness

The forms make intent explicit and the type-system checks are correspondingly strict; the C-style cast remains available but is conventionally avoided. The full treatment is in Types.

new and delete

new allocates and constructs; delete destructs and deallocates:

Widget *w = new Widget(arg1, arg2);
delete w;

int *arr = new int[100];
delete[] arr;

The bare forms allocate from the free store (the implementation’s heap). Placement new constructs at a caller-provided address:

char buffer[sizeof(Widget)];
Widget *w = new (buffer) Widget(arg1, arg2);
w->~Widget();           // explicit destruction

In modern C++, new and delete rarely appear in user code; smart pointers (std::unique_ptr, std::shared_ptr) and value-semantic types replace them. The full treatment of memory and ownership is in Memory and RAII.

throw, typeid, and the coroutine operators

throw raises an exception:

if (denominator == 0) throw std::runtime_error("division by zero");

typeid yields a runtime representation of a type:

const std::type_info &t = typeid(*pointer);
std::cout << t.name() << '\n';

The coroutine operators co_await, co_yield, and co_return mark suspension points in a coroutine; they are part of C++20’s coroutine surface and are treated briefly in Functions.

Operator overloading

Most operators may be overloaded for class types and enumeration types. The conventions for each:

OperatorConventional useNotes
+, -, *, /, %Arithmetic on user-defined number-like typesOverload as non-member binary functions taking const T&
+=, -=, *=, /=, %=Compound assignmentOverload as member; return *this
==, !=EqualitySince C++20, defining == synthesises !=
<, <=, >, >=OrderingSince C++20, prefer <=> and let the compiler derive
<<, >>Stream insertion / extraction; bitwise shift on integersOverload as non-member; first parameter is std::ostream& or std::istream&
&, |, ^, ~Bitwise on integer-like types; flag-set operations on enums
=Copy and move assignmentAlways a member; canonically T& operator=(const T&) and T& operator=(T&&)
()Function-call operatorDefines a function object; central to lambdas and STL predicates
[]SubscriptMember; canonical pair: T& operator[](size_t) and const T& operator[](size_t) const
*, ->DereferenceMember; the principal pattern is the smart-pointer interface
++, --Increment / decrementDistinguish prefix and postfix by signature
,CommaAlmost never overloaded; surprising semantics
&&, ||Logical AND / ORAlmost never overloaded; lose short-circuit evaluation
new, deleteCustom allocatorsMember or namespace-scope; rare

A small number of operators may not be overloaded: ::, ., .*, ?:, sizeof, typeid, alignof. The reason in each case is that the operator is part of the language’s grammar in a way that admits no user-supplied implementation.

A canonical example

class Money {
public:
    Money(long cents) : cents_(cents) {}

    Money &operator+=(const Money &other) {
        cents_ += other.cents_;
        return *this;
    }

    long cents() const { return cents_; }

private:
    long cents_;
};

Money operator+(Money lhs, const Money &rhs) {
    lhs += rhs;
    return lhs;
}

bool operator==(const Money &lhs, const Money &rhs) {
    return lhs.cents() == rhs.cents();
}

auto operator<=>(const Money &lhs, const Money &rhs) {
    return lhs.cents() <=> rhs.cents();
}

The pattern — define += as a member returning *this, then implement + as a non-member that takes the left-hand side by value and uses += — is the canonical form for arithmetic types.

User-defined literals

C++11 admits user-defined literal suffixes:

constexpr long double operator"" _km(long double v)  { return v * 1000.0; }
constexpr long double operator"" _mi(long double v)  { return v * 1609.34; }

auto distance = 3.0_km + 1.5_mi;

The <chrono> library uses the mechanism extensively (5s, 100ms, 1h); the <string> library provides s for std::string ("hello"s).

The conditional operator

The ternary ?: operator selects between two expressions:

int  max  = (a > b) ? a : b;
auto plural = (n == 1) ? "" : "s";

The two result expressions must have a common type, found by the standard’s elaborate conversion rules. Mixing types unwisely produces compilation errors — cond ? 1 : "two" is a type error.

The comma operator

The comma operator evaluates its left operand, discards the result, evaluates the right operand, and yields the right operand’s value:

for (int i = 0, j = n; i < j; ++i, --j) /* … */;

The principal use is the for loop’s init and step expressions where the language allows only one expression. Outside that context, the comma operator is rare; ordinary commas in argument lists, declarators, and braced-init lists are separators, not the operator.

sizeof, alignof, typeid

sizeof(T) and sizeof expr yield the size in bytes; alignof(T) yields the alignment requirement; typeid(expr) yields a std::type_info reference describing the runtime type:

std::cout << sizeof(int) << '\n';     // implementation-defined (commonly 4)
std::cout << alignof(double) << '\n'; // commonly 8
std::cout << typeid(*ptr).name() << '\n';

sizeof does not evaluate its operand; only its type is examined. typeid may evaluate its operand if the operand is a polymorphic type and the result needs runtime resolution.

Sequence points and unsequenced evaluations

C++17 refined the C11 model. The principal rules:

  • Each full expression is followed by a sequence point.
  • Function arguments are indeterminately sequenced: each is fully evaluated before the call, but the order is unspecified.
  • C++17 added that the postfix part of a function call (f(x).g(y)) sequences the arguments to g after the result of f(x). Before C++17 this was unspecified.
  • Unsequenced modifications of the same scalar object remain undefined behaviour.

The practical advice is unchanged from C: do not modify the same object twice in a single expression, and do not modify an object whose value is also being read in the same expression.

Lvalues, rvalues, and value categories

C++ refines C’s lvalue/rvalue distinction into a five-category model: lvalue, prvalue, xvalue, glvalue, rvalue. The categories are introduced because move semantics requires distinguishing temporaries (which can be moved from) from named objects (which cannot, without explicit std::move).

The full treatment is in Move semantics. For the operator surface, the relevant distinction is which operators yield lvalues (most member access, dereference, subscript) and which yield rvalues (most arithmetic, comparison, logical operators).

Operator precedence

The full precedence table from highest to lowest:

LevelOperatorsAssociativity
1::left
2++ -- (postfix), () (call), [], ., ->, typeid, named castsleft
3++ -- (prefix), + - (unary), ! ~, * (deref), & (addr), (type), sizeof, alignof, new, delete, co_awaitright
4.*, ->*left
5* / %left
6+ - (binary)left
7<< >>left
8<=>left
9< <= > >=left
10== !=left
11& (bitwise)left
12^left
13|left
14&&left
15||left
16?:, throw, co_yield, assignmentsright
17,left

Three precedence facts are worth memorising:

  1. & and | (bitwise) bind less tightly than == and !=. if (x & MASK == 0) does not test what it appears to.
  2. The named casts bind at the postfix level, like a function call: static_cast<T>(x).method() is (static_cast<T>(x)).method().
  3. << and >> are the bitwise shifts at level 7, but for stream operations they retain the same precedence — which means os << a, b is (os << a), b, and the comma operator silently discards b.

When in doubt, parenthesise. The C++ compiler does not penalise redundant parentheses; the alternative is to invite the bug.