Skip to content
VLSI Mentor

C++ Part I · Module 14 · Smart Pointers and Ownership

Raw Pointers in a Modern Codebase: Observers, Not Owners

Six chapters of this module have argued for smart pointers, which makes it easy to draw the wrong conclusion: that raw pointers are a thing you have now finished with, and that a codebase is modern in proportion to how few of them it contains. That conclusion is wrong, and acting on it produces worse code than the problem it was trying to avoid. A raw pointer has a precise remaining job — it is the type that says I need to see this object and I am not responsible for it — and no smart pointer says that more clearly or more cheaply. This chapter is the argument, with the costs measured: observing through a raw pointer compiles to two instructions, through a weak_ptr to thirty-one plus an out-of-line call and an atomic. Then the honest half. A raw observer can dangle, and this chapter measures one doing exactly that, printing a plausible wrong number from storage that had already been reused. But the measurement that actually decides the design is the comparison with the previous chapter: AddressSanitizer caught the dangling observer immediately and named the line, while the reference cycle of Chapter 14.6 was invisible to the same tool and to the leak detector. So reaching for shared ownership to feel safe trades a bug your instruments find for one they cannot represent — and the thing that should decide between a raw pointer and a weak_ptr is neither taste nor caution, but a specific question about which object is destroyed first.

Advanced20 min readC++Part IFundamentalsOwnership

Part I · Module 14 · Chapter 14.7 · Raw Pointers in a Modern Codebase: Observers, Not Owners

1. The Question This Module Has Been Circling

Chapter 14.6 §16 put it directly. A weak_ptr solved the cycle by expressing I need to see this object but I do not own it — and that is the same sentence Chapter 14.1 §5 attached to a const T& parameter and a plain T*.

So three types say the same thing, and the module has used all three without ever stating when each is right. The answer is not about style:

A raw pointer is the right observer when the observed object is guaranteed to outlive the observation. A weak_ptr is the right observer when it is not.

Everything else in this chapter is the evidence for that sentence and the measurement of what each choice costs.

2. Raw Pointers Are Not Legacy

What this module actually removed from raw pointers was one job: owning. Chapter 10.9 §6 measured that a raw pointer cannot express ownership — the information lives in a comment — and Chapters 14.2 through 14.6 replaced it with types that can.

Ownership was never a raw pointer's only job. The others are untouched:

  • observing an object someone else owns;
  • being optionally absent, which a reference cannot be;
  • being rebindable, which a reference also cannot be;
  • crossing a C API boundary, where nothing else exists.

A codebase with no raw pointers at all has usually achieved it by making non-owning relationships into owning ones, which is how Chapter 14.6's cycles get built.

3. The Five Signatures, Measured

Five ways to take a Model, and what each does to the caller's ownership:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    static void takesOwnership (std::unique_ptr<Model> m);
    static void sharesOwnership(std::shared_ptr<Model> m);
    static void observesRef    (const Model& m);
    static void observesPtr    (const Model* m);
    static void mutatesRef     (Model& m);
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
observe, then still own:
    observesRef      id=7
    observesPtr      id=7
    mutatesRef       set id=99
    owner still holds id=99
share:
    sharesOwnership  id=7 use_count=2
    after the call, use_count=1
give it away:
    takesOwnership   id=99
    owned.get()=0x0

Three distinct outcomes.

The observers left ownership alone. After three calls, including one that modified the object, the owner still held it — id=99 proves mutatesRef wrote through to the real object rather than a copy, and the owner is still the owner.

Sharing raised the count inside the call — use_count=2 — and dropped it back to 1 on return. The callee was a temporary co-owner.

And taking emptied the caller — owned.get()=0x0, Chapter 14.3 §5's measurement. Chapter 14.1 §5 measured that the same call without std::move does not compile.

Notice which two signatures never mention a smart pointer. Observation does not need one, and that is the whole case for this chapter.

4. What Observation Costs

Three ways to read one field, at -O2:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    int viaRaw      (const M* m)                        { return m->v; }
    int viaWeak     (const std::weak_ptr<M>& w)         { auto p = w.lock(); return p ? p->v : -1; }
    int viaSharedRef(const std::shared_ptr<M>& s)       { return s->v; }
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
viaRaw                :  2 instructions, no atomics
viaWeak               : 31 instructions, including ldaddal
viaSharedRef          :  3 instructions, no atomics

sizeof(const M*)      :  8
sizeof(weak_ptr<M>)   : 16
sizeof(shared_ptr<M>) : 16

Two instructions for the raw pointer — a load and a return, Chapter 14.2 §5's result again.

Thirty-one for the weak_ptr, and the honest reading is worse than the number. That 31 excludes the body of a call it contains:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    bl   __ZNSt3__119__shared_weak_count4lockEv     ; out of line — not counted
    ...
    ldaddal x9, x8, [x8]                            ; x9 = -1, the atomic decrement
    blr  x8                                         ; virtual destructor dispatch
    bl   __ZNSt3__119__shared_weak_count14__release_weakEv

lock() is not inlined, so its own compare-exchange loop on the strong count is outside the 31. The atomic visible here carries −1: it is the decrement when the temporary shared_ptr that lock() returned is destroyed. There is also a virtual dispatch and a second call. So the real ratio is worse than 15×.

And three for the shared_ptr by reference — one more than the raw pointer, which is the reference indirection, not smart-pointer overhead. There is no refcount traffic at all, because nothing was copied. This distinction is worth care: comparing a raw pointer against a const shared_ptr& and calling the extra load overhead would be a mis-measurement of the reference, not of the pointer.

The sizes matter where observers are stored rather than passed: a cached raw back-pointer is 8 bytes, a cached weak_ptr is 16.

5. So Why Not Always Raw?

Because a raw observer can outlive what it observes. A cached observer, four cases:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    struct RawObserver  { const Model* m = nullptr;    void look() const; };
    struct WeakObserver { std::weak_ptr<Model> m;      void look() const; };
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
A. raw observer, owner ALIVE (the normal case):
     raw observer sees id=7
B. weak observer, owner ALIVE:
     weak observer sees id=7
C. weak observer, owner DESTROYED first:
     weak observer: owner is gone -- declined safely
D. raw observer, owner DESTROYED first:
     raw observer sees id=5
   (that read was undefined behaviour)

A and B are identical, which is §4's point: in the normal case the raw pointer bought the same answer for a fifteenth of the work.

C is what a weak_ptr is for. It asked, found the object gone, and declined — Chapter 14.6 §7's lock() returning empty.

And D is the cost. The object held 42. The observer printed 5. Not 42, not zero, not a crash — a plausible small integer from storage that had already been reused for something else. Chapter 10.7 §4 measured the identical shape and called that section It Printed 5; this is the same bug reached through a smart-pointer-using codebase, because the observer was raw.

6. What the Tools Said This Time

Case D under AddressSanitizer:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
ERROR: AddressSanitizer: heap-use-after-free on address 0x6020000000f0
SUMMARY: AddressSanitizer: heap-use-after-free observer.cpp:9 in RawObserver::look() const

Caught immediately, with the function and the line. Not a hint — a precise diagnosis of the exact read.

Now hold that next to Chapter 14.6 §4:

FailureTool verdict
dangling raw observer (this chapter)heap-use-after-free, file and line
shared_ptr reference cycle (14.6)0 leaks for 0 total leaked bytes; ASan detect_leaks unsupported

That table is the most consequential thing in this chapter, and it inverts the intuitive argument. "Use a shared_ptr everywhere so nothing can dangle" does not remove the failure mode — it exchanges it. You give up a bug that a standard tool finds on the first run and names the line of, and you take on one that is invisible to both of this platform's instruments and detectable only by reading the code.

Chapter 14.6 §5 explained why the asymmetry is structural rather than a tooling gap: a dangling read touches freed memory, which a sanitizer can see, while a cycle is reachable from itself, which a reachability scanner correctly reports as fine.

7. The Discriminator

So the choice is not raw-versus-safe. It is a question about lifetimes:

Is the observed object guaranteed to outlive the observation?

Yes → raw pointer (or a reference). The guarantee usually comes from structure rather than from hope:

  • a parameter observed only for the duration of the call — the owner is in the caller's frame, as in §3;
  • a member of an object the owner outlives by construction — a child observing a parent that owns it;
  • a back-pointer in a tree whose parent owns its children (Chapter 14.6 §10's first row);
  • anything where the owner is destroyed after the observer, by scope nesting.

No, or you cannot tell → weak_ptr. The honest cases:

  • the observer is stored and the owner's lifetime is decided elsewhere;
  • a cache or registry of things that may be released at any time;
  • an observer list where subjects come and go independently;
  • anything crossing a thread boundary, where "first" is not well defined.

The useful move when this is unclear is to notice that the question has an answer in the design, and that not knowing it is information. Chapter 14.3 §10's callout made the same point about fighting a unique_ptr: if you cannot say which of two objects dies first, that is a gap in the design, and weak_ptr is the correct, honest response to it rather than a workaround.

8. T*, T&, or weak_ptr

Given the answer is "raw", two forms remain, and the choice is about absence:

NeedUse
must refer to an object, never absentT& or const T&
may be absent, or must be rebindableT* or const T*
stored, and the object may already be gonestd::weak_ptr<T>

Prefer the reference where it fits — Chapter 14.1 §5 measured both working identically, and a reference cannot be null, so one failure mode disappears from the function body. Use the pointer when nullptr genuinely means something, which §3's observesPtr showed with its m ? m->id : -1.

(std::optional<T&> is not available in C++17, and library types that encode "never null" in a pointer's type exist but are not standard. Module 21 covers the standard utility vocabulary.)

And one rule for all three: get() is correct for producing an observer. Chapter 14.2 §7 measured it leaving ownership untouched, in contrast to release(). Passing owner.get() to an observing function is not a code smell — it is the documented way to hand out a non-owning view.

9. What a Raw Observer Must Never Do

Four things, all of which turn an observer back into a pretend owner:

Never delete it. You did not allocate it and you are not responsible for it.

Never store it past the lifetime you reasoned about in §7. A T* parameter that gets cached in a member has silently changed the question from "for this call" to "indefinitely", and the §7 answer may now be different.

Never return it from a function whose local owns the object. Chapter 8.8 §3 measured that dangle.

And never put it in a container that outlives the owners. Chapter 14.1 §9 measured vector<Model*> leaking three objects — that was the owning misuse; the symmetric observing misuse is a vector of observers to objects already destroyed.

10. What to Actually Do

Use a raw pointer or reference to observe when the owner outlives the observation — the common case.

Prefer const T& where the thing can never be absent; const T* where it can.

owner.get() is the correct way to produce an observer.

Use weak_ptr when the observer is stored and the owner's lifetime is decided elsewhere.

Answer §7's question explicitly rather than choosing a type to avoid it.

Never delete, cache, return or store a raw observer beyond the lifetime you reasoned about.

And do not treat shared_ptr everywhere as a safety measure — measured, it trades a detectable bug for an undetectable one.

11. Mistakes Worth Making Once

Believing raw pointers are obsolete. They lost one job — owning — and kept the others.

Reaching for shared_ptr so nothing can dangle. You get cycles instead, which no tool here can see.

Caching a T* parameter in a member. The lifetime question silently changed.

deleteing an observed pointer. You are not the owner.

Taking const shared_ptr<T>& in an observer. Measured 3 instructions against 2, and the signature now mentions ownership for no reason.

Taking shared_ptr<T> by value to observe. Two atomics per call (Chapter 14.5 §5).

Using weak_ptr for a parameter. The owner is in the caller's frame for the whole call — §7's first row.

Comparing a raw pointer against a const shared_ptr& and calling the extra load overhead. It is the reference.

Returning a T* to a local's object. Chapter 8.8 §3.

Using release() to make an observer. Chapter 14.2 §7.

12. Interview Relevance

"Do raw pointers still have a place in modern C++?" Yes — observing, optional absence, rebinding, and C APIs. They lost ownership, not existence. Answering "no" is a weak signal.

"How do you pass an object to a function that only reads it?" const T&, or const T* if it can be absent — never a smart pointer.

"When do you need weak_ptr instead of a raw pointer?" When the observer is stored and cannot guarantee the owner outlives it. The discriminator is a lifetime question, not a safety preference.

"What does observing through a weak_ptr cost?" Measured 31 instructions against 2 — and that 31 excludes an out-of-line lock() call containing its own compare-exchange loop, plus an atomic decrement when the temporary dies.

"Is shared_ptr everywhere a safe default?" No — it exchanges a dangling read, which ASan names with file and line, for a reference cycle, which leaks reports as 0 leaks. This is a strong discriminator.

"Is calling .get() a code smell?" Not for producing an observer. It is a smell when the result is stored, deleted, or used to construct a second owner.

13. What You Should Be Able to Say Now

  1. Raw pointers lost one job — owning — and kept observing, optional absence, rebinding and C interop.
  2. Measured: two observer signatures left the owner owning, and mutatesRef wrote through — owner still holds id=99.
  3. Measured: sharing gave use_count=2 inside and 1 after; taking gave owned.get()=0x0.
  4. Observation does not need a smart pointer — neither observing signature mentions one.
  5. Measured: viaRaw 2 instructions, viaWeak 31, viaSharedRef 3.
  6. The 31 excludes an out-of-line lock() call with its own compare-exchange loop, plus an atomic decrement and a virtual dispatch — so the real ratio is worse.
  7. viaSharedRef's extra instruction is the reference indirection, not overhead — no refcount traffic occurs.
  8. Sizes: raw 8, weak_ptr 16 — relevant when observers are stored.
  9. Measured: with the owner alive, the raw and weak observers gave identical answers.
  10. Measured: with the owner destroyed, the weak observer declined safely; the raw one printed 5 for an object that held 42.
  11. Measured: ASan caught the dangling read as heap-use-after-free, with file and line.
  12. Chapter 14.6's cycle was invisible — 0 leaks, and detect_leaks unsupported.
  13. So shared_ptr everywhere trades a detectable bug for an undetectable one.
  14. The discriminator is: does the observed object outlive the observation? Yes → raw; no or unknown → weak_ptr.
  15. Prefer T& where absence is impossible, T* where it is meaningful.
  16. get() is the correct way to produce an observer, and never to produce an owner.

14. Exercises

Exercise 1 — the four jobs. List what a raw pointer lost and what it kept, with one example each.

Exercise 2 — the five signatures. For each of §3's five, say what happens to the caller's ownership, citing the measured line.

Exercise 3 — the cost table. Give the three instruction counts, and explain why viaSharedRef is 3 rather than 2 without calling it overhead.

Exercise 4 — predict the output. A cached raw observer whose owner was destroyed in an inner scope. Say what the program prints, what the standard says about it, and what ASan reports.

Exercise 5 — the trade. A colleague proposes making every pointer a shared_ptr so nothing dangles. Give the measured counter-argument using both verdicts from §6's table.

Exercise 6 — apply the discriminator. Choose raw, reference, or weak_ptr: a function that validates a model; a child node's pointer to its parent; a registry of models created and destroyed by other subsystems; an observer list where subjects unregister themselves; a pointer handed to a worker thread.

Exercise 7 — reference or pointer. For each raw answer above, choose T& or T* and say why.

Exercise 8 — the four prohibitions. For each of §9's four, give a concrete code shape that violates it and the consequence.

Exercise 9 — get() or not. Three uses of owner.get(): passed to an observer; stored in a member for later; used to construct a second unique_ptr. Say which is correct and what the others do.

15. Summary

  • Raw pointers are not legacy. They lost ownership and kept observing, optional absence, rebinding and C interop.
  • Measured: observers left ownership untouched while still writing through — owner still holds id=99.
  • Measured: observing costs 2 instructions raw against 31 through a weak_ptr — and the 31 excludes an out-of-line lock() call, an atomic decrement and a virtual dispatch.
  • const shared_ptr&'s extra instruction is the reference, not overhead — a distinction worth getting right.
  • Measured: with the owner alive both observers agreed; with it destroyed, the weak one declined safely and the raw one printed 5 for an object holding 42.
  • Measured: ASan named the dangling read with file and line; Chapter 14.6's cycle was invisible to every tool here.
  • So shared_ptr everywhere is a trade, not a safety net — a detectable bug exchanged for an undetectable one.
  • The discriminator is a lifetime question: does the observed object outlive the observation?
  • Prefer T&, then T*, then weak_ptr, and use get() to make observers, never owners.

Each of the last six chapters has produced a rule about one type, and the rules now conflict often enough to need adjudicating. A function that takes a model: by value, by reference, by raw pointer, by unique_ptr, by const shared_ptr&? A class that holds one: as a member, a unique_ptr, a shared_ptr? Six defensible answers and one correct one, decided not by preference but by what the function or class is for — and the useful discovery is that the right signature is almost always forced once you write down two things, who owns the object and whether the callee's use outlives the call. The last chapter of this module is that decision procedure, applied to the signatures and class designs you will actually be asked to write, including the measurement of what happens when you get it wrong in the direction everybody gets it wrong.

Standards & specifications

Governing standard
ISO/IEC 14882 (the C++ language standard)(opens ISO/IEC (JTC 1/SC 22/WG 21) in a new tab)

Defines the C++ language and its standard library — the type system and templates, the object model and object lifetime, initialisation and overload resolution, the memory model and the containers, algorithms and utilities the library provides. Like C's standard it is a FAMILY of editions (C++98/03, C++11, C++14, C++17, C++20, C++23) and the editions are NOT interchangeable: a feature this curriculum relies on may be unavailable, differently specified or merely proposed in another edition, so each lesson names the edition its claim depends on. It does not define any foreign-function interface to a hardware description language, nor what a compiler actually emits, nor platform behaviour on real silicon — those are the implementation's and the device's contracts. Working drafts published by WG21 are freely available and are what most engineers read day to day; a draft is not the published standard.

This page also covers RTL structure, verification approach and debugging technique. Those are engineering practice built on the standard, not requirements the standard itself imposes.

Where this fits

Part of the C++ Programming curriculum.