Contact
Evil Genius SocietyHomeLearnForumLeaderboardSocialsMessages
← Back to module

Lesson 1 of 6

Every heap allocation raises one question, and it is not a syntax question:

Who is responsible for destroying this, and when?

Answer that first, and the code writes itself. Skip it, and no amount of careful deleteing will save you — you will be guessing at run time about a decision nobody wrote down.

The owner is whoever must release the resource. Everyone else is a user, and users must not free anything.

The four answers

There are only four, and C++ has a type for each:

| Answer | Use | Cost | |---|---|---| | Nobody — it is just a value | the object itself | none | | Exactly one thing owns it | std::unique_ptr<T> | none over a raw pointer | | Several things share it | std::shared_ptr<T> | an atomic counter | | This is a view, not an owner | T* or T& | none |

Notice what is missing: "a raw pointer that owns something." That is the option Modules 8 and 9 spent their time showing you the failures of.

Why the question is hard

Consider three plausible designs for a program managing tasks:

#include <iostream>
#include <string>
#include <vector>

struct Task {
    std::string name;
    int priority;
};

// Design 1: the container owns the tasks outright.
class TaskList {
    std::vector<Task> tasks;

public:
    void add(std::string name, int priority) {
        tasks.push_back(Task{std::move(name), priority});
    }

    std::size_t size() const { return tasks.size(); }

    // A non-owning view. The caller must not keep this past the next add().
    const Task* highest_priority() const {
        if (tasks.empty()) {
            return nullptr;
        }
        const Task* best = &tasks[0];
        for (const Task& t : tasks) {
            if (t.priority > best->priority) {
                best = &t;
            }
        }
        return best;
    }
};

int main() {
    TaskList list;
    list.add("write lesson", 3);
    list.add("fix bug", 9);
    list.add("reply to email", 1);

    const Task* urgent = list.highest_priority();
    if (urgent) {
        std::cout << "most urgent: " << urgent->name
                  << " (" << urgent->priority << ")\n";
    }
    std::cout << "count: " << list.size() << "\n";
    return 0;
}
most urgent: fix bug (9)
count: 3

TaskList owns everything. highest_priority() returns a raw pointer that owns nothing — a view. That is a correct, modern use of a raw pointer.

But note the comment: the caller must not keep it past the next add(), which may reallocate the vector (lesson 8.5). The ownership design is sound; the lifetime of the view is a separate contract the type system does not enforce.

When one owner is not enough

Now suppose two different lists need to refer to the same task, and the task must live as long as either list does. A std::vector<Task> cannot express that: each vector owns its own copies.

That is the case shared ownership exists for, and lesson 10.3 covers it. The important thing is that you arrive there by answering the question, not by reaching for shared_ptr because it sounds safe.

The default order to consider

Work down this list and stop at the first one that fits:

1. A plain value. Most things do not need to be on the heap at all. Task task; inside a std::vector<Task> needs no pointer of any kind.

2. std::unique_ptr<T>. One owner, transferable. This covers nearly every remaining case. Use it when the object must outlive the scope that made it, must be polymorphic (Module 16), or is genuinely too large for the stack.

3. std::shared_ptr<T>. Only when the lifetime genuinely depends on several independent owners, and you cannot say in advance which finishes last.

4. A raw T* or T&. For parameters and returns that only look at something. Never for ownership.

Reaching for shared_ptr first is the single most common mistake people make on arriving here. It is not "the safe one" — it is the one that says "I do not know who owns this," which is usually a statement about the design rather than about the problem.

Ownership in function signatures

The type you accept tells the caller what you will do with the object:

#include <iostream>
#include <memory>
#include <string>

struct Report {
    std::string title;
};

// "I only look at it." Cannot be null.
void print(const Report& r) {
    std::cout << "printing " << r.title << "\n";
}

// "I only look at it, and it might not exist."
void print_if_any(const Report* r) {
    if (r) {
        std::cout << "printing " << r->title << "\n";
    } else {
        std::cout << "nothing to print\n";
    }
}

// "I take ownership. After calling this, it is mine."
void archive(std::unique_ptr<Report> r) {
    std::cout << "archived " << r->title << "\n";
}   // destroyed here

int main() {
    auto report = std::make_unique<Report>(Report{"Q3 results"});

    print(*report);              // still ours
    print_if_any(report.get());  // still ours
    print_if_any(nullptr);

    archive(std::move(report));  // handed over — must be explicit

    std::cout << "we still hold a pointer: " << (report != nullptr) << "\n";
    return 0;
}
printing Q3 results
printing Q3 results
nothing to print
archived Q3 results
we still hold a pointer: 0

Read the call sites. print(*report) and print_if_any(report.get()) clearly borrow. archive(std::move(report)) clearly transfers — you cannot call it without writing std::move, so the handover is visible at the call site and in code review.

That is the real benefit of smart pointers: ownership becomes something the compiler checks and a reader can see, rather than a convention in a comment.

Try it

For each of these, decide which of the four answers fits, and why:

  1. A Point with two doubles, used as a local variable.
  2. A window in a GUI, held by the application, referred to by several widgets that must not keep it alive.
  3. A configuration object read by many parts of a program, created at startup, destroyed at shutdown.
  4. A node in a linked list that its list owns.
  5. A cached texture used by several sprites, freed when the last sprite stops using it.

Checkpoint

  • Ask who destroys this, and when before choosing a type.
  • The owner releases the resource; everyone else is a non-owning user.
  • Four answers: a value, unique_ptr, shared_ptr, or a non-owning T*/T&.
  • "A raw pointer that owns something" is not on the list.
  • Consider them in that order and stop at the first that fits.
  • shared_ptr is for when you genuinely cannot name the last owner — not a default.
  • The parameter type documents and enforces whether a function borrows or takes ownership.

Questions about this lesson

Log in to ask a question.