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.

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)
  • gsl::not_null<T*> β€” GSL type that enforces non-nullability at construction, orthogonal to observer_ptr's non-ownership annotation
  • C++ Core Guidelines I.11 β€” never transfer ownership by raw pointer or reference