Lesson 6: Essential Operations

Lesson 0006 — A Tour of C++, Chapter 6 (§6.1–§6.7)

Chapter 5 gave you Vector with a constructor and destructor — but a class with a destructor is only half the story. Chapter 6 completes it: constructors, destructors, and the copy and move operations form a matched set that must be designed together, or the class suffers logical or performance problems. This chapter explains the full complement of "essential operations," why the default memberwise copy is a disaster for a resource handle like Vector, how move semantics makes returning large objects cheap, and how explicit, =default, and =delete give you precise control over what the compiler generates. It closes with conventional operations (==, swap(), hash<>), user-defined literals, and the "rule of zero" — the guiding principle that makes all of this fit together.

Why essential operations matter now: Chapter 5's Vector was carefully designed around RAII, but it was still incomplete — it copied by default, which was subtly wrong. This chapter shows the whole matched set and the rule of zero: most classes need no user-defined special members at all. You'll also see why move semantics (the && from §5.2.3, now fully explained) is what makes returning containers by value cheap, and why the standard library builds everything on it.

6.1 Introduction — The Essential Operations

Some operations are fundamental: initialization, assignment, copy, and move — the language rules make assumptions about them. Others, like == and <<, have conventional meanings that it's perilous to ignore. Both kinds are covered in this chapter.

6.1.1 A Matched Set

Constructors, destructors, and copy and move operations are not logically separate — they must be designed as a matched set. If a class X has a destructor that performs a nontrivial task (free-store deallocation, lock release), the class is likely to need the full complement:

class X {
public:
    X(Sometype);              // ''ordinary constructor'': create an object
    X();                      // default constructor
    X(const X&);              // copy constructor
    X(X&&);                   // move constructor
    X& operator=(const X&);   // copy assignment: clean up target and copy
    X& operator=(X&&);        // move assignment: clean up target and move
    ~X();                     // destructor: clean up
    // ...
};

There are five situations in which an object can be copied or moved: as the source of an assignment, as an object initializer, as a function argument, as a function return value, and as an exception. Assignment uses a copy or move assignment operator; in principle the other cases use a copy or move constructor — but in practice the compiler often elides the copy by constructing the object directly in its target:

X make(Sometype);
X x = make(value);   // compiler typically constructs the X from make() directly in x

Except for the ordinary constructor, these special member functions are generated by the compiler as needed. You can be explicit with =default and =delete:

class Y {
public:
    Y(Sometype);
    Y(const Y&) = default;   // I really do want the default copy constructor
    Y(Y&&) = default;        // and the default move constructor
    // ...
};

A =delete makes any attempted use a compile-time error — and it can suppress any function, not just the essential members. The classic case is a base class in a hierarchy, which must not be copied memberwise:

class Shape {
public:
    Shape(const Shape&) = delete;   // no copying
    Shape& operator=(const Shape&) = delete;
    // ...
};
void copy(Shape& s1, const Shape& s2) {
    s1 = s2;   // error: Shape copy is deleted
}

The rule of zero: define all of the essential operations or none (using the defaults for all). A class whose members are themselves well-behaved (like Vector and string) needs nothing:

struct Z {
    Vector v;
    string s;
};
Z z1;              // default initialize z1.v and z1.s
Z z2 = z1;         // default copy z1.v and z1.s — correct, because Vector's copy is correct

The one caution: when a class has a pointer member, it's usually a good idea to be explicit about copy and move — a pointer may point to something the class must delete (memberwise copy would be wrong) or must not delete (the reader wants to know). §6.2.1 shows the concrete failure.

6.1.2 Conversions

A constructor taking a single argument defines a conversion from its argument type. For complex that's ideal:

complex z1 = 3.14;   // z1 becomes {3.14, 0.0}
complex z2 = z1 * 2; // z2 becomes z1*{2.0,0} == {6.28, 0.0}

But the same mechanism is dangerous for Vector:

Vector v1 = 7;   // OK: v1 has 7 elements — but is that what the reader meant?

The standard-library vector does not allow this int-to-vector "conversion." The fix is explicit:

class Vector {
public:
    explicit Vector(int s);   // no implicit conversion from int to Vector
    // ...
};
Vector v1(7);   // OK: v1 has 7 elements
Vector v2 = 7;  // error: no implicit conversion from int to Vector

More types are like Vector than like complex: declare single-argument constructors explicit unless there's a good reason not to.

6.1.3 Member Initializers

A data member can carry its own default value — a default member initializer — used whenever a constructor doesn't provide one:

class complex {
    double re = 0;   // representation: two doubles with default value 0.0
    double im = 0;
public:
    complex(double r, double i) :re{r}, im{i} {}   // construct from two scalars: {r,i}
    complex(double r) :re{r} {}                    // construct from one scalar: {r,0}
    complex() {}                                   // default complex: {0,0}
    // ...
};

This simplifies the constructors and, more importantly, makes it impossible to accidentally leave a member uninitialized.

6.2 Copy and Move

By default, objects can be copied — user-defined types as well as built-ins. The default meaning of copy is memberwise: copy each member. For a simple concrete type like complex, that is exactly the right semantics. For a resource handle like Vector, it is a disaster; for abstract types it almost never makes sense. When you design a class, you must always consider if and how it might be copied.

6.2.1 Copying Containers

When a class is a resource handle — responsible for an object accessed through a pointer — the default memberwise copy violates its invariant. Two Vectors would share the same elements:

void bad_copy(Vector v1) {
    Vector v2 = v1;   // copy v1's representation into v2
    v1[0] = 2;        // v2[0] is now also 2!
    v2[1] = 3;        // v1[1] is now also 3!
}

The fact that Vector has a destructor is a strong hint that default memberwise copy is wrong — the compiler should at least warn. The fix is a proper copy constructor and copy assignment that give each Vector its own copy of the elements:

Vector::Vector(const Vector& a)   // copy constructor
    :elem{new double[a.sz]},      // allocate space for elements
     sz{a.sz}
{
    for (int i = 0; i != sz; ++i) // copy elements
        elem[i] = a.elem[i];
}
Vector& Vector::operator=(const Vector& a)   // copy assignment
{
    double* p = new double[a.sz];            // allocate new space first
    for (int i = 0; i != a.sz; ++i)
        p[i] = a.elem[i];
    delete[] elem;                           // delete old elements
    elem = p;
    sz = a.sz;
    return *this;                            // *this: the object the member fn was called on
}

Note the order in the copy assignment: the new elements are copied before the old ones are deleted, so if the copy throws an exception, the original Vector is preserved — the strong exception-safety guarantee.

6.2.2 Moving Containers

Copying is correct but can be costly for large containers. References avoid the cost when passing into a function, but you can't return a reference to a local object — it's destroyed before the caller sees it. Consider operator+:

Vector operator+(const Vector& a, const Vector& b) {
    if (a.size() != b.size()) throw Vector_size_mismatch{};
    Vector res(a.size());
    for (int i = 0; i != a.size(); ++i)
        res[i] = a[i] + b[i];
    return res;   // the local res is about to die — we want to move it out
}

With only copy operations, r = x+y+z copies a Vector at least twice — embarrassing for 10,000 doubles. And the copies are pure waste: res is never used again after the return. The move operations fix that:

class Vector {
public:
    Vector(const Vector& a);             // copy constructor
    Vector& operator=(const Vector& a);  // copy assignment
    Vector(Vector&& a);                  // move constructor
    Vector& operator=(Vector&& a);       // move assignment
    // ...
};

Vector::Vector(Vector&& a)   // move constructor
    :elem{a.elem},           // ''grab the elements'' from a
     sz{a.sz}
{
    a.elem = nullptr;        // now a has no elements
    a.sz = 0;
}

The && is an rvalue reference — a reference to a value you can't assign to (an rvalue, like an integer returned from a function). Since nobody else can use that value, the move constructor may safely "steal" it. A move constructor does not take a const argument — it is supposed to remove the value from its argument. And a move leaves the moved-from object in a state that allows a destructor to run (and typically allows assignment too, as the standard-library algorithms assume).

Where the programmer knows a value won't be used again but the compiler can't prove it, use std::move() — which doesn't actually move anything; it's a cast to an rvalue reference:

Vector f() {
    Vector x(1000), y(2000), z(3000);
    z = x;               // we get a copy (x might be used later in f())
    y = std::move(x);    // we get a move (move assignment)
    // ... better not use x here ...
    return z;            // we get a move
}

Finally, copy elision: the standard obliges the compiler to eliminate most copies associated with initialization, so move constructors are invoked less often than you might imagine. But assignments are not elided, so move assignment can be critical for performance.

6.3 Resource Management

With the full set of essential operations, a programmer has complete control over a contained resource's lifetime — and a move constructor lets an object move cheaply from one scope to another. Things you can't or wouldn't want to copy out of a scope (a thread holding a concurrent activity; a Vector of a million doubles) can be moved out instead:

std::vector<thread> my_threads;
Vector init(int n) {
    thread t {heartbeat};                    // run heartbeat concurrently
    my_threads.push_back(std::move(t));      // move t into my_threads
    Vector vec(n);
    for (auto& x : vec) x = 777;
    return vec;                              // move vec out of init()
}
auto v = init(1'000'000);                    // start heartbeat and initialize v

Resource handles like Vector and thread are superior to direct use of built-in pointers — in fact, the standard library's unique_ptr is itself a resource handle. Done thoroughly, this achieves strong resource safety: no leaks for a general notion of resource — memory, locks, sockets, file handles, thread handles. Non-memory resources are called non-memory resources, and a good resource management system handles all kinds.

Stroustrup's stance on garbage collection: it's the last choice, after cleaner and better-localized alternatives are exhausted — "my ideal is not to create any garbage, thus eliminating the need for a garbage collector: Do not litter!" Garbage collection is fundamentally global, and locality matters more as systems become more distributed. Instead: use resource handles, give each resource an owner in some scope, release at the end of the owner's scope (RAII), move resources between scopes with move semantics or smart pointers. In the standard library, RAII is pervasive: string, vector, map, ifstream, thread, lock_guard, unique_ptr, shared_ptr.

6.4 Operator Overloading

We can give meaning to most of C++'s operators for user-defined types — that's operator overloading. It is not possible to define new operators (**, ===, unary %…); allowing that would cause as much confusion as good. And operators should be defined with conventional semantics — an operator+ that subtracts would do nobody any good.

Overloadable: binary arithmetic (+ - * / %), bitwise (& | ^), relational (== != < <= > >= <=>), logical (&& ||), unary (+ - ~ !), assignments (= += *=), increments and decrements (++ --), pointer operations (->, unary *, unary &), call (), subscript [], comma, and shift << >>. You cannot define operator . to get "smart references."

An operator can be defined as a member function — conventionally for operators that modify their first operand, and (for historical reasons) required for =, ->, (), and []:

class Matrix {
public:
    Matrix& operator=(const Matrix& a);   // assign a to *this; return a reference to *this
    // ...
};

Or as a free-standing function — conventional for operators with symmetric operands, so both operands are treated identically (§5.2.1):

Matrix operator+(const Matrix& m1, const Matrix& m2);   // return the sum

Returning a potentially large object relies on move semantics (§6.2.2) for good performance.

6.5 Conventional Operations

Some operations have conventional meanings that programmers and the standard library assume: comparisons (§6.5.1), container operations (size(), begin(), end(), §6.5.2), iterators and smart pointers (§6.5.3), I/O (>> and <<, §6.5.4), swap() (§6.5.5), and hash functions hash<> (§6.5.6). Conform to them when designing types for which they make sense.

6.5.1 Comparisons

The meaning of equality is closely related to copying: after a copy, the copies should compare equal.

X a = something;
X b = a;
assert(a == b);   // if a!=b here, something is very odd (§4.5)

If you define ==, also define != so that a!=b means !(a==b). If you define <, define the rest so the usual equivalences hold: a<=b means (a<b)||(a==b); a>b means b<a; and so on. To treat both operands identically, define binary operators as free-standing functions in the namespace of the class.

The spaceship operator <=> is a law onto itself. Defining the default <=> implicitly defines all the other relational operators:

class R {
    // ...
    auto operator<=>(const R& a) const = default;
};
void user(R r1, R r2) {
    bool b1 = (r1 <=> r2) == 0;   // r1==r2
    bool b2 = (r1 <=> r2) < 0;    // r1<r2
    bool b3 = (r1 <=> r2) > 0;    // r1>r2
    bool b4 = (r1 == r2);          // implicitly defined
    bool b5 = (r1 < r2);           // implicitly defined
}

Like C's strcmp(), <=> implements three-way comparison: negative means less-than, 0 equal, positive greater-than. If <=> is defined non-default, == is not implicitly defined — but < and the other relational operators are. The standard library's string and vector follow the default pattern (lexicographical order), and often provide a separate optimized == — comparing strings for equality can stop at a length check, while <=> must read all characters to determine the order.

6.5.2 Container Operations

Unless there is a really good reason not to, design containers in the style of the standard-library containers: resource-safe, implemented as a handle with the appropriate essential operations. Standard containers know their number of elements (size()), and — more importantly — support iterators, pairs delimiting a sequence:

for (size_t i = 0; i != c.size(); ++i)   // index-based traversal
    c[i] = 0;

for (auto p = c.begin(); p != c.end(); ++p)   // iterator-based
    *p = 0;

for (auto& x : c)   // range-for — built on begin() and end()
    x = 0;

begin() points at the first element; end() points one-past-the-last. Iterators support ++ (next) and * (access) like pointers, and are used to pass sequences to standard algorithms:

sort(v.begin(), v.end());

The const versions are called cbegin() and cend(). This iterator model gives great generality and efficiency (details in Chapters 12 and 13).

6.5.3 Iterators and "smart pointers"

User-defined iterators and smart pointers implement the operators and aspects of a pointer desired for their purpose: access (*, ->, []), iteration and navigation (++, --, +=, -=, +, -), and copy/move (=) — often adding semantics as needed.

6.5.4 Input and Output Operations

For integers, << is left-shift and >> is right-shift; for iostreams they are the output and input operators (§1.8, Chapter 11). Same tokens, completely different conventional meanings, selected by overload resolution.

6.5.5 swap()

Many algorithms, most notably sort(), use a swap() that exchanges two objects' values — and they assume it is very fast and doesn't throw. The standard library provides std::swap(a,b) implemented as three move operations. If your type is expensive to copy and could plausibly be swapped, give it move operations or a swap() or both.

6.5.6 hash<>

The standard-library unordered_map<K,V> is a hash table. To use a type X as a key, you must define hash<X>; for common types like std::string the standard library defines it for you.

6.6 User-Defined Literals

One purpose of classes is to mimic built-in types — and built-ins have literals: 123 is an int, 0xFF00u is an unsigned int, "Surprise!" is a const char[10]. User-defined literals (UDLs) give the same convenience to your types:

auto s1 = "Surprise!"s;   // std::string
auto t  = 123s;           // seconds (chrono)
auto z  = 12.7i + 47;     // imaginary 12.7i plus 47 → complex {47, 12.7}
Header Namespace Suffixes
<chrono> std::literals::chrono_literals h, min, s, ms, us, ns
<string> std::literals::string_literals s
<string_view> std::literals::string_literals sv
<complex> std::literals::complex_literals i, il, if

UDLs are defined with literal operators, which convert a literal of their argument type followed by a suffix into their return type:

constexpr complex<double> operator""i(long double arg)   // imaginary literal
{
    return {0, arg};
}

operator"" introduces a literal operator; the suffix (i) follows it; the argument type (long double) says the suffix applies to floating-point literals; the return type (complex<double>) is the literal's type. Since both the suffix implementation and + are constexpr, 2.7182818 + 6.283185i is computed at compile time.

6.7 Advice

Here is a summary of the guidance from this chapter. All 19 items, with the section where each is introduced. The C++ Core Guidelines link each item to its recommended practice.

# Guideline §
1 Control construction, copy, move, and destruction of objects. 6.1.1
2 Design constructors, assignments, and the destructor as a matched set of operations. 6.1.1
3 Define all essential operations or none. 6.1.1
4 If a default constructor, assignment, or destructor is appropriate, let the compiler generate it. 6.1.1
5 If a class has a pointer member, consider if it needs a user-defined or deleted destructor, copy and move. 6.1.1
6 If a class has a user-defined destructor, it probably needs user-defined or deleted copy and move. 6.2.1
7 By default, declare single-argument constructors explicit. 6.1.2
8 If a class member has a reasonable default value, provide it as a data member initializer. 6.1.3
9 Redefine or prohibit copying if the default is not appropriate for a type. 6.1.1
10 Return containers by value (relying on copy elision and move for efficiency). 6.2.2
11 Avoid explicit use of std::copy(). 16.6
12 For large operands, use const reference argument types. 6.2.2
13 Provide strong resource safety; that is, never leak anything that you think of as a resource. 6.3
14 If a class is a resource handle, it needs a user-defined constructor, a destructor, and nondefault copy operations. 6.3
15 Manage all resources — memory and non-memory — using RAII. 6.3
16 Overload operations to mimic conventional usage. 6.5
17 If you overload an operator, define all operations that conventionally work together. 6.1.1, 6.5
18 If you define <=> for a type as non-default, also define ==. 6.5.1
19 Follow the standard-library container design. 6.5.2

Retrieval Quiz

The rule of zero

What does the rule of zero say about a class like struct Z { Vector v; string s; };?

Why memberwise copy fails for Vector

Why is the default memberwise copy a disaster for Vector (but fine for complex)?

The copy assignment order

Why does Vector::operator= allocate the new array and copy elements before calling delete[] elem?

explicit constructors

Why does Vector v2 = 7; fail to compile when the constructor is declared explicit Vector(int s)?

Move vs. copy semantics

What does Vector::Vector(Vector&& a) do, and why is a not const?

When move is chosen

In y = std::move(x); (vs. z = x;) — what is std::move(x) actually doing?

The spaceship operator

What does a default auto operator<=>(const R& a) const = default; give you?

Operator overloading limits

Which of these is NOT possible in C++ operator overloading?

User-defined literals

What does constexpr complex<double> operator""i(long double arg) enable?


Notes

Essential operations :-

essential ops ctor, dtor, copy ctor/assign, move ctor/assign

designed as matched set define all or none (rule of zero)

5 copy/move situations assignment | initializer | argument | return | exception

=default keep compiler version ; =delete forbid use compile error

pointer member be explicit about copy/move

Conversions :-

single-arg ctor implicit conversion (eg int complex)

explicit only direct init Vector v2 = 7 error

use explicit by default more types like Vector than complex

Member initializers :-

default member initializer double re = 0; used when ctor omits

benefit no accidentally-uninitialized member

Copy :-

default copy memberwise right for complex, wrong for Vector

resource handle copy × memberwise two handles share one array double-delete

copy ctor new array + copy elements

copy assign allocate & copy first, delete old after strong guarantee on throw

Move :-

rvalue ref && ; binds to values nobody else can assign

move ctor steal handle null out source dtor still safe

move args × const moving removes the value

std::move cast to rvalue ref ; selects move ops

copy elision init copies eliminated ; assignments not

Resource management :-

resource anything acquired/released (eg memory, locks, sockets, threads)

RAII owner in scope ; released at scope end

GC last choice ; ''Do not litter!'' ; locality matters

strong resource safety no leaks of any resource

Operator overloading :-

overload existing ops × new tokens (eg **)

conventional semantics mimic built-in usage

member modifies first operand ; =, ->, (), [] required member

free-standing symmetric operands (eg +)

Conventional ops :-

== define with != ; copy must compare equal

<=> three-way ; default all relational ops

non-default <=> < etc implicit, == not

container size(), begin(), end() ; iterators for algorithms

swap() 3 moves ; fast, no-throw for sort()

hash<> needed for unordered_map keys

User-defined literals :-

UDL suffix on literal (eg "x"s, 123s, 12.7i)

literal operator operator""suffix(arg)

constexpr compile-time computation


Primary source: Stroustrup, B. (2022). A Tour of C++, 3rd ed., Chapter 6: "Essential Operations." Addison-Wesley.
Reference: Chapter 1–6 Quick Reference & Glossary — keep it beside you while you study.
Recommended supplement: cppreference on copy constructor, move constructor, operator overloading, defaulted comparisons, and user-defined literals.

Questions? Ask your agent — your teacher — about anything unclear: why the rule of zero beats hand-writing special members, how the move constructor interacts with std::vector's reallocation, or when std::move is worth writing explicitly. Follow-ups are expected, not optional.

Chapters

  1. Lesson 1: The Basics (Ch. 1)
  2. Lesson 2: User-Defined Types (Ch. 2)
  3. Lesson 3: Separate Compilation (Ch. 3)
  4. Lesson 4: Error Handling (Ch. 4)
  5. Lesson 5: Classes (Ch. 5)
  6. Lesson 6: Essential Operations (Ch. 6 — this lesson)
  7. Lesson 7: Templates (Ch. 7)
  8. Lesson 8: Concepts and Generic Programming (Ch. 8)
  9. Lesson 9: Library Overview (Ch. 9)
  10. Lesson 10: Strings and Regular Expressions (Ch. 10)
  11. Lesson 11: Input and Output (Ch. 11)
  12. Lesson 12: Containers (Ch. 12)
  13. Lesson 13: Algorithms (Ch. 13)
  14. Lesson 14: Ranges (Ch. 14)
  15. Lesson 15: Pointers and Containers (Ch. 15)
  16. Lesson 16: Utilities (Ch. 16)
  17. Lesson 17: Numerics (Ch. 17)
  18. Lesson 18: Concurrency (Ch. 18)
  19. Lesson 19: History and Compatibility (Ch. 19)
← Course Dashboard ← Ch. 5: Classes Ch. 7: Templates → Quick Reference →