Skip to content
C++
Library
Basic

observer_ptr

A non-owning pointer wrapper from the C++ Library Fundamentals TS that makes non-ownership explicit at the type level.

observer_ptrsince C++ LFTS v2 (experimental)

A non-owning, nullable pointer wrapper that makes the absence of ownership explicit in the type system, serving as a vocabulary type for "I observe this object but do not manage its lifetime."

Overview

std::experimental::observer_ptr<T> was introduced in the C++ Library Fundamentals Technical Specification v2 (ISO/IEC TS 19568:2017). It addresses a persistent expressiveness gap in C++: a raw T* carries no indication of whether the pointer owns the pointed-to resource. In codebases mixing owning and non-owning raw pointers, intent must be conveyed through naming conventions or comments β€” both fragile and invisible to static analysis.

observer_ptr<T> wraps a raw pointer without affecting the lifetime of the pointee. Its destructor performs no cleanup. Its purpose is communicative: a function accepting observer_ptr<Widget> signals to callers that it will not delete, store with extended lifetime, or transfer ownership of the Widget.

As of C++26, observer_ptr has not been merged into any ISO C++ standard. It remains under std::experimental in <experimental/memory>. Both libstdc++ and libc++ provide it under the experimental namespace, but stability guarantees apply only within a given compiler release. For production libraries with broad ABI concerns, isolate the dependency behind a project-internal alias.

The type is a zero-overhead abstraction at runtime. The cost is purely mechanical: slightly more verbose call sites in exchange for ownership intent that is searchable, toolable, and enforced by the compiler rather than a style guide.

Syntax

cpp
#include <experimental/memory>

namespace stdx = std::experimental;

// Construction
stdx::observer_ptr<T> p;                // null by default
stdx::observer_ptr<T> p{raw_ptr};      // observing raw_ptr
auto p = stdx::make_observer(raw_ptr);  // factory β€” deduces T

// Core interface
T*  p.get();          // raw pointer; never throws
T&  *p;               // dereference; UB if null
T*  p.operator->();  // member access; UB if null
    p.reset();        // set to null
    p.reset(q);       // observe a different pointer
T*  p.release();      // set to null, return old pointer

// Conversion
explicit operator bool() const noexcept;  // null check
explicit operator T*()   const noexcept;  // explicit cast to raw pointer

// Non-member β€” comparison uses the underlying pointer value
bool operator==(observer_ptr<T>, observer_ptr<T>) noexcept;
bool operator< (observer_ptr<T>, observer_ptr<T>) noexcept;
// ... !=, <=, >, >= follow

The explicit conversion to T* is intentional friction. It surfaces implicit conversions to raw pointers during review and prevents accidental drops back into untyped pointer territory.

Examples

Replacing ambiguous raw pointer members

The canonical problem: a class with both owning and non-owning pointer fields, indistinguishable from the declaration alone.

cpp
// Before: raw pointers everywhere β€” intent is implicit
class Renderer {
    Shader*  m_shader;   // owns? borrows?
    Context* m_context;  // ???
};

// After: ownership visible at the type level
#include <memory>
#include <experimental/memory>

namespace stdx = std::experimental;

class Renderer {
    std::unique_ptr<Shader>       m_shader;   // C++11 β€” owns, deletes on destruction
    stdx::observer_ptr<Context>   m_context;  // borrows β€” destruction is a no-op

public:
    explicit Renderer(Context& ctx)
        : m_shader{std::make_unique<Shader>()}  // C++14
        , m_context{stdx::make_observer(&ctx)}
    {}
};

The destructor of Renderer destroys m_shader via unique_ptr and leaves ctx entirely untouched β€” now self-evident from member types without a comment in sight.

Non-owning function parameters

cpp
#include <experimental/memory>
namespace stdx = std::experimental;

// The signature contracts: we observe scene and cam, never outlive them
void render_frame(stdx::observer_ptr<Scene>  scene,
                  stdx::observer_ptr<Camera> cam)
{
    if (!scene || !cam) return;
    cam->apply();
    scene->draw();
}

// Call site: make_observer converts raw pointer to observer
auto scene = std::make_unique<Scene>();   // C++14
auto cam   = std::make_unique<Camera>();
render_frame(stdx::make_observer(scene.get()),
             stdx::make_observer(cam.get()));

Compare with void render_frame(Scene*, Camera*). The raw-pointer version is identical at runtime but loses the semantic contract and cannot be distinguished from a function that might store the pointer.

Non-owning observer lists

cpp
#include <vector>
#include <algorithm>
#include <experimental/memory>

namespace stdx = std::experimental;

class EventBus {
    std::vector<stdx::observer_ptr<IListener>> m_listeners;

public:
    void subscribe(IListener& l) {
        m_listeners.push_back(stdx::make_observer(&l));
    }

    void unsubscribe(IListener& l) {
        auto target = stdx::make_observer(&l);
        // operator== compares wrapped pointer values
        m_listeners.erase(
            std::remove(m_listeners.begin(), m_listeners.end(), target),
            m_listeners.end());
    }

    void dispatch(const Event& e) {
        for (auto& obs : m_listeners) {
            if (obs) obs->on_event(e);
        }
    }
};

vector<observer_ptr<IListener>> self-documents that EventBus holds references, not lifetime claims. std::remove works because operator== compares the wrapped pointers by value.

Isolating the experimental dependency

In headers that form part of a library interface, avoid exposing std::experimental directly:

cpp
// project/memory.hpp
#if __has_include(<experimental/memory>)
#  include <experimental/memory>
   namespace project {
       template<typename T>
       using observer_ptr = std::experimental::observer_ptr<T>;

       template<typename T>
       auto make_observer(T* p) noexcept {
           return std::experimental::make_observer(p);
       }
   }
#else
#  error "observer_ptr unavailable β€” upgrade compiler or use gsl::not_null"
#endif

Callers depend on project::observer_ptr<T>. Switching to a standardized version later requires one edit.

Best Practices

Use it at API boundaries. The value is communicative, not mechanical. It pays off most at function signatures and class member declarations where ownership ambiguity is highest. Internal implementation details can remain raw pointers without undermining the idiom.

Prefer make_observer over the constructor. auto p = stdx::make_observer(raw) deduces T and reads uniformly alongside make_unique and make_shared, reinforcing the smart pointer idiom at the call site.

Treat release() as an escape hatch, not a pattern. release() resets the observer to null and returns the old raw pointer. Its primary use is adapting to legacy C-style APIs that demand a T*. Routine use undermines the ownership narrative the type creates.

Check nullability at the point of acquisition, not deep in call chains. observer_ptr is nullable by default. If your precondition requires a non-null pointer, enforce it at the call site with assert or switch to gsl::not_null<T*>, which enforces non-nullability at construction time and is complementary to observer_ptr for different invariants.

Common Pitfalls

Confusing it with std::weak_ptr. weak_ptr (C++11) is tied to shared_ptr-managed objects and can detect object destruction via expired() and lock(). observer_ptr has no such mechanism β€” it cannot detect whether the pointee is still alive. If you need dangling-pointer detection, use weak_ptr. If you only need to express non-ownership, use observer_ptr.

Dangling observers. There is no safety net. An observer_ptr does not know whether the observed object has been destroyed.

cpp
stdx::observer_ptr<Widget> stale;
{
    Widget w;
    stale = stdx::make_observer(&w);
}
// w is destroyed β€” stale is now a dangling pointer
// Dereferencing stale is undefined behaviour

Ownership discipline must still be enforced at the architectural level; observer_ptr annotates the contract, it does not enforce it.

Unexpected compile errors from explicit conversion. Engineers accustomed to raw pointer idioms are occasionally surprised that passing an observer_ptr<T> to a function expecting T* does not compile:

cpp
void legacy_api(Widget*);

auto obs = stdx::make_observer(&w);
legacy_api(obs);              // error: no implicit conversion to Widget*
legacy_api(obs.get());        // correct
legacy_api(static_cast<Widget*>(obs));  // also correct, more verbose

This is intentional design, not a deficiency. The friction surfaces unintentional drops back to raw pointer territory and makes such conversions visible during review.

Do not expose std::experimental in stable library headers. Features under std::experimental carry no ABI or API stability guarantees between compiler releases. Typedef behind a project namespace before exposing in any public header.


C++26 Status and Alternatives

observer_ptr was proposed for standardization (N4282, P0468) but has never been merged into the C++ IS. As of C++26, the paper remains withdrawn. The core objections from the committee were:

  • release() semantics conflict with the non-owning premise. A non-owning pointer type that exposes release() raises questions about what "releasing" means when there is nothing to release. The name implies transfer of ownership, which is conceptually contradictory for an observer.
  • Nullable vs. non-nullable tension. The committee could not agree on whether the canonical non-owning pointer should enforce non-nullability (like a reference) or permit null (like a raw pointer). observer_ptr chose nullable, but a significant faction argued the more useful primitive is a non-null non-owning pointer.
  • std::span and concepts displaced part of the motivation. C++20's std::span<T> (non-owning view over contiguous data) and ranges fill many use cases that previously motivated observer_ptr, narrowing its niche.

Despite stagnation in the standard, the annotation idiom it represents is well-established. The ecosystem has converged on several complementary alternatives.

Decision table: choosing a non-owning pointer type

SituationRecommended type
Non-owning reference, null is a valid state, no GSL dependencyobserver_ptr<T> (project alias)
Non-owning reference, null must be forbidden, GSL availablegsl::not_null<T*>
Owning raw pointer in legacy code that cannot use smart pointersgsl::owner<T*>
Internal implementation detail, ownership clear from contextT* with a documentation convention

gsl::not_null<T*> (Guidelines Support Library) enforces non-nullability at construction: assigning null causes an assertion or exception. It is the right choice when a precondition of a function is that the pointer must be valid. It does not express non-ownership on its own β€” that information comes from the absence of a delete call and from documentation.

gsl::owner<T*> is the annotation for the opposite case: a raw pointer that does own the resource but cannot yet use unique_ptr (common in C-style APIs or memory pools). Static analysis tools use this annotation to verify that owner<T*> parameters are always deleted on all code paths.

Plain T* with a naming convention (p_, _obs, _ref suffix) is the zero-dependency fallback. It communicates nothing to the compiler or analysis tools, but is acceptable in implementation files where ownership is clear from the surrounding code and team conventions are enforced through review.


Roll-Your-Own Without <experimental/memory>

Projects that want the type annotation without depending on a potentially-unstable experimental header can implement a minimal observer_ptr<T> in about 30 lines. This is also useful when targeting toolchains that do not ship <experimental/memory> (MSVC before 2019, some embedded toolchains).

cpp
// project/observer_ptr.hpp β€” C++11
#pragma once
#include <cstddef>      // std::ptrdiff_t
#include <functional>   // std::hash
#include <utility>      // std::swap

namespace project {

template<typename T>
class observer_ptr {
public:
    using element_type = T;

    // Construction
    constexpr observer_ptr() noexcept : m_ptr(nullptr) {}
    constexpr explicit observer_ptr(T* p) noexcept : m_ptr(p) {}
    template<typename U>
    constexpr observer_ptr(observer_ptr<U> other) noexcept : m_ptr(other.get()) {}

    // Core observers
    constexpr T* get()        const noexcept { return m_ptr; }
    constexpr T& operator*()  const noexcept { return *m_ptr; }
    constexpr T* operator->() const noexcept { return m_ptr; }
    constexpr explicit operator bool() const noexcept { return m_ptr != nullptr; }
    constexpr explicit operator T*()   const noexcept { return m_ptr; }  // C++11

    // Modifiers
    constexpr void reset(T* p = nullptr) noexcept { m_ptr = p; }
    constexpr T*   release()             noexcept { T* tmp = m_ptr; m_ptr = nullptr; return tmp; }
    constexpr void swap(observer_ptr& other) noexcept { std::swap(m_ptr, other.m_ptr); }

private:
    T* m_ptr;
};

// Factory β€” mirrors std::make_unique / std::make_shared naming  // C++14
template<typename T>
constexpr observer_ptr<T> make_observer(T* p) noexcept {
    return observer_ptr<T>{p};
}

// Comparison β€” ordered by pointer value for use in sorted containers
template<typename T, typename U>
bool operator==(observer_ptr<T> a, observer_ptr<U> b) noexcept { return a.get() == b.get(); }
template<typename T, typename U>
bool operator!=(observer_ptr<T> a, observer_ptr<U> b) noexcept { return !(a == b); }
template<typename T, typename U>
bool operator< (observer_ptr<T> a, observer_ptr<U> b) noexcept { return a.get() <  b.get(); }
template<typename T, typename U>
bool operator<=(observer_ptr<T> a, observer_ptr<U> b) noexcept { return !(b < a); }
template<typename T, typename U>
bool operator> (observer_ptr<T> a, observer_ptr<U> b) noexcept { return b < a; }
template<typename T, typename U>
bool operator>=(observer_ptr<T> a, observer_ptr<U> b) noexcept { return !(a < b); }

} // namespace project

// std::hash specialization β€” enables use as unordered_map key  // C++11
namespace std {
template<typename T>
struct hash<project::observer_ptr<T>> {
    std::size_t operator()(project::observer_ptr<T> p) const noexcept {
        return std::hash<T*>{}(p.get());
    }
};
} // namespace std

This implementation is API-compatible with std::experimental::observer_ptr for the common subset of operations. The explicit conversion operator to T* (line marked C++11) is the key design choice: it prevents silent drops to raw pointer at implicit conversion sites while still allowing static_cast<T*>(obs) and obs.get() at deliberate use sites. Once your compiler ships a stable observer_ptr, swapping project::observer_ptr for the standard version is a one-line typedef change.


Static Analysis Tool Support

The ownership annotations that observer_ptr, gsl::not_null, and gsl::owner provide are not merely documentation β€” several production-grade static analysis tools can read and verify them, turning a naming convention into a machine-checked invariant.

Clang Static Analyzer with CPPCoreCheck rules understands gsl::owner<T*> and will warn when an owner parameter is not freed on all paths. When combined with [[clang::lifetimebound]] attributes (C++20), it can also detect when an observer_ptr or raw non-owning pointer escapes the scope of the object it points to.

PVS-Studio uses the GSL annotations as hints for its ownership analysis. A function parameter typed gsl::owner<Widget*> that is never deleted, or a gsl::not_null<T*> that is conditionally null-checked (implying the caller was uncertain about nullability), both generate diagnostics.

SonarQube C++ plugin tracks ownership flow interprocedurally and uses the presence or absence of owner annotations to detect resource leaks and unmatched allocations.

The annotations are most powerful at callback registration boundaries, where an object subscribes to events and the analysis tool needs to verify that the observer does not outlive the subject:

cpp
// C++17 β€” annotating a callback registration with ownership intent
#include <gsl/pointers>   // Guidelines Support Library

class Button {
public:
    // owner registers a raw callback β€” NOT stored beyond the call
    void on_click(gsl::not_null<IClickHandler*> handler) {
        handler->handle_click();   // called immediately, not stored
    }

    // Store for later β€” observer_ptr signals non-ownership to analysis tools
    void set_tooltip_provider(project::observer_ptr<ITooltipProvider> provider) {
        m_tooltip = provider;      // stored, but not owned
    }

private:
    project::observer_ptr<ITooltipProvider> m_tooltip;  // must not outlive Button
};

The contrast between not_null<IClickHandler*> (used immediately, no storage concern) and observer_ptr<ITooltipProvider> (stored member, lifetime coupling) communicates different risk profiles to both human reviewers and static analysis passes. Tools that understand these annotations flag dangerous patterns β€” such as constructing a local ITooltipProvider and passing its address to set_tooltip_provider β€” that would otherwise require interprocedural alias analysis to catch.


gsl::not_null Comparison

gsl::not_null<T*> and observer_ptr<T> solve adjacent but distinct problems. observer_ptr<T> is nullable by default and expresses non-ownership. gsl::not_null<T*> expresses non-nullability but says nothing about ownership. The two annotations compose: the most expressive combination is a gsl::not_null<project::observer_ptr<T>>, though in practice the pattern is usually encoded at the API boundary through parameter types rather than nested generics.

gsl::not_null enforces its invariant at construction: assigning a null pointer either terminate()s (the default under most implementations) or throws, depending on the GSL configuration. This moves the null-check from the body of every function that receives the pointer to a single point of entry.

cpp
// C++17
#include <gsl/pointers>
#include "project/observer_ptr.hpp"

// IRenderer has two kinds of optional dependencies:
// - m_logger: may be absent (null is valid) β€” use observer_ptr
// - m_device: must always be present β€” use not_null
class IRenderer {
public:
    explicit IRenderer(gsl::not_null<IRenderDevice*> device,
                       project::observer_ptr<ILogger>   logger = {})
        : m_device(device)   // null check happens here; constructor throws/terminates if null
        , m_logger(logger)   // null is fine β€” logging is optional
    {}

    void render_frame() {
        if (m_logger) m_logger->log("render_frame begin");  // nullable β€” must check
        m_device->submit_commands();                         // not_null β€” no check needed
    }

private:
    gsl::not_null<IRenderDevice*>   m_device;  // always valid
    project::observer_ptr<ILogger>  m_logger;  // may be null
};

// Call sites illustrate the contract difference:
IRenderer r1{device.get()};              // logger omitted β€” null observer, fine
IRenderer r2{device.get(), stdx::make_observer(&logger)};  // both provided

// This would terminate/throw at construction, not deep inside render_frame:
// IRenderer bad{nullptr};

The not_null wrapper eliminates the defensive null-check boilerplate inside render_frame for m_device. Any code path that reaches the constructor with a null device fails immediately and loudly, rather than crashing silently when the null pointer is first dereferenced. The observer_ptr member retains nullability because logging is genuinely optional β€” the null check on m_logger in render_frame is deliberate, not defensive.


Array Observers

std::experimental::observer_ptr is defined for single-object pointers only. The standard TS does not provide a partial specialization for observer_ptr<T[]>, and there is no operator[] for array observation. For contiguous sequences, std::span<T> (C++20) is almost always the right answer: it bundles pointer and length together and provides operator[], iterators, and subspan operations.

There are, however, situations where std::span is not available or not appropriate β€” legacy interfaces that accept a T* plus a length separately, embedded environments still on C++17, or code that must remain API-compatible with a C library that separates the two. For those cases, a partial specialization that adds operator[] is straightforward:

cpp
// project/observer_ptr.hpp β€” add below the primary template  // C++11

namespace project {

template<typename T>
class observer_ptr<T[]> {
public:
    using element_type = T;

    constexpr observer_ptr() noexcept : m_ptr(nullptr) {}
    constexpr explicit observer_ptr(T* p) noexcept : m_ptr(p) {}

    constexpr T*   get()                           const noexcept { return m_ptr; }
    constexpr T&   operator[](std::ptrdiff_t i)    const noexcept { return m_ptr[i]; }
    constexpr explicit operator bool()             const noexcept { return m_ptr != nullptr; }
    constexpr explicit operator T*()               const noexcept { return m_ptr; }

    constexpr void reset(T* p = nullptr) noexcept { m_ptr = p; }

private:
    T* m_ptr;
    // Note: no operator* or operator-> β€” not meaningful for arrays
};

template<typename T>
constexpr observer_ptr<T[]> make_array_observer(T* p) noexcept {
    return observer_ptr<T[]>{p};
}

} // namespace project

The specialization deliberately omits operator* and operator->: dereferencing the first element of an array without an index is legal C++ but is almost always a bug when the intent is array observation. Forcing callers to use operator[] makes the array-nature explicit.

cpp
// C++17 β€” using observer_ptr<T[]> with a legacy C callback interface
extern "C" void process_samples(float* data, int count);  // C API

void analyse(project::observer_ptr<float[]> samples, int n) {
    // operator[] β€” clear intent, no pointer arithmetic
    float peak = samples[0];
    for (int i = 1; i < n; ++i)
        if (samples[i] > peak) peak = samples[i];

    // Adapt to C API with explicit cast β€” friction is intentional
    process_samples(static_cast<float*>(samples), n);
}

// Caller retains ownership of the buffer
std::vector<float> buf = load_audio();
analyse(project::make_array_observer(buf.data()), static_cast<int>(buf.size()));

Prefer std::span<T> over observer_ptr<T[]> when targeting C++20 or later. std::span is a first-class vocabulary type in the standard with richer semantics (bounds, subspans, iterators) and does not require a project-internal implementation. The observer_ptr<T[]> specialization above is for teams that cannot yet adopt C++20 but want the ownership annotation benefit for array parameters in legacy codebases.


See Also

  • std::unique_ptr β€” single-owner smart pointer with exclusive lifetime control (C++11)
  • std::shared_ptr β€” shared ownership with atomic reference counting (C++11)
  • std::weak_ptr β€” non-owning reference to a shared_ptr-managed object with expiry detection (C++11)
  • std::span<T> β€” non-owning view over a contiguous sequence with bounds and iterators (C++20)
  • gsl::not_null<T*> β€” GSL type that enforces non-nullability at construction, orthogonal to observer_ptr's non-ownership annotation
  • gsl::owner<T*> β€” GSL annotation marking a raw pointer as owning, for use in legacy code that cannot adopt smart pointers
  • C++ Core Guidelines I.11 β€” never transfer ownership by raw pointer or reference
  • C++ Core Guidelines F.7 β€” for general use, take T* or T& arguments rather than smart pointers