21.3 unique_ptr és egyedi tulajdonlás

Az előző részben (RAII wrapper file descriptorhoz és lockhoz (21.2)) egy kézzel írt RAII wrapperrel kötöttük össze a fájlleíró élettartamát egy objektum élettartamával. Ez a minta helyes volt, de minden erőforrástípushoz külön osztályt igényel: destruktort, move konstruktort, move értékadást, törölt másolást. A standard library erre a leggyakoribb esetre — egyetlen, dinamikusan létrehozott objektum vagy egy külső, pointerrel azonosított erőforrás — kész, jól bejárt megoldást ad: a std::unique_ptr-t. Ez az alfejezet azt mutatja meg, milyen szerződést kell ehhez a típushoz illeszteni, és hol bukik el a látszólag ártalmatlan get()/release() használat.

21.3.1 Az egyedi tulajdonlás szerződése

A C memóriamodelljében (lásd Tulajdonlási szerződés és cleanup ágak (8.4)) a tulajdonlás puszta konvenció: a malloc-ot és a free-t ugyanannak a kódágnak kell összehangolnia, és a fordító ezt nem ellenőrzi. A C++ ezt a konvenciót típussá teszi.

Definíció. A std::unique_ptr<T, D> egy move-only, azaz mozgatással átadható, de nem másolható osztálysablon, amely egy T* értéket és egy D típusú deletert tárol. A tárolt pointer az objektum tulajdonában van: a unique_ptr destruktora a deletert a pointerrel meghívja, és a unique_ptr élettartama alatt a szabvány szerződése szerint egyetlen más unique_ptr sem birtokolhatja ugyanazt az objektumot.

Előfeltételek és feltételek. - A unique_ptr másoló konstruktora és másoló értékadása törölt; tulajdonlást csak mozgatással lehet átadni. - A mozgatott-forrás (moved-from) állapota definiált: üres lesz, get() == nullptr. Ez nem “véletlenszerű szemét”, hanem szerződéses posztkondíció. - Az üres unique_ptr legális és normális; a deletert ilyenkor nem hívja meg (a reset() null pointerre hatástalan). - A unique_ptr nem garantálja, hogy a mutatott objektum érvényes: a mutatott objektumot bárki törölheti, ha nyers pointerhez jut. A típus a tulajdonlás tényét kódolja, nem a teljes élettartam-biztonságot.

Ez utóbbi pont a fejezet egyik fő határa: a smart pointer nem old meg minden lifetime-hibát. Ha egy observer (T*, T&, iterátor) túlél egy reset()-et, move-ot vagy a owner megszűnését, a hiba ugyanúgy dangling hivatkozás lesz, mintha C-ben dolgoznánk.

21.3.2 unique_ptr<T> és unique_ptr<T[]>

A két specializáció nem ugyanaz a típus, és nem konvertálható egymásba.

Tulajdonság unique_ptr<T> unique_ptr<T[]>
Alapértelmezett deleter std::default_delete<T> → delete p std::default_delete<T[]> → delete[] p
operator*, operator-> van nincs
operator[] nincs van (C++20-tól indexelés)
Tipikus forrás new T(...), make_unique<T>(...) new T[n], make_unique<T[]>(n)
Méretezés szemantikája egy objektum tömbtömb, a hossz nem tárolt

A unique_ptr<T[]> nem tudja a tömb hosszát: a delete[] a fordító által eltárolt könyvelésből dolgozik, a size() fogalma nem létezik. Ezért a hosszt külön kell vinni (például std::span-ban, lásd span, byte és bounds-szerződések (31.3)) — a unique_ptr<T[]> csak a felszabadításért felel.

Egy fontos részlet: std::make_unique<T[]>(n) a new T[n]-t value-initializálja, azaz skalár típusra nullázza, osztályra a default konstruktort hívja. A nyers new T[n] ezzel szemben default-initializál, ami skalár típusnál határozatlan értéket hagy. A make_unique tehát itt nem csak kényelem, hanem inicializálási garancia is.

21.3.3 make_unique és a konstrukció kivételbiztonsága

A unique_ptr legfontosabb hozadéka, hogy a tulajdonlás már az objektum létrejötte pillanatában kialakul. Nyers new-val ez nem így van:

// ROSSZ tanítási példa: nem ez a helyes változat
use_widget(new Widget(cfg), compute_limit());   // compute_limit() dobhat

A sorrend itt nem garantált, hogy a Widget átadása előbb megtörténik-e: ha a new Widget lefut, majd a compute_limit() kivételt dob, a lefoglalt Widget* semmihez sem tartozik, és elszivárog. A hívási kifejezés nem fejeződik be, így a paraméterként átvett, deklarált paraméter destruktora sem fut le. C++17 óta az argumentumkiértékelés sorrendje indeterminately sequenced, de ez nem zárja ki a szivárgást, mert a kivétel a hívás előtt történik, amikor a pointer még senki tulajdona.

A helyes változat:

auto w = std::make_unique<Widget>(cfg);   // innentől owner
use_widget(w.get(), compute_limit());     // ha dob, w destruktora lefut

A make_unique a new-ot és a unique_ptr konstrukcióját egyetlen kifejezésbe zárja, így a new sikere és a tulajdonba vétel között nincs kivételes pont. Ugyanezért ne írjunk std::unique_ptr<T>(new T(...)) alakot: működik, de elveszíti a kifejezés-szintű védelmet, és a make_unique rövidebb is.

A C++20-as alapból visszafelé is van fallback: a make_unique C++14-es, tehát C++20-cal mindenképp elérhető. Ami viszont C++20-hoz kötött: a capture nélküli lambda típusa default-konstruálható, így lambda használható deleterként anélkül, hogy külön struktúrát írnánk — C++17-es vagy korábbi profilban ehhez nevesített functor kell.

21.3.4 get, reset, release: a tulajdonlás átadásának eszközei

Művelet Tulajdonlás Pointer Mikor használd
get() marad változatlan átadás nem tulajdonló API-nak
operator*, operator-> marad változatlan közvetlen használat
reset(p) marad, új pointerre régi felszabadul, új bekerül újratöltés, „reseat”
reset() megszűnik üres lesz korai felszabadítás
release() átadódik a hívónak a unique_ptr üres lesz csak indokolt FFI/ABI határon
move konstruktor/értékadás átadódik forrás üres lesz owner átadása

A release() a legveszélyesebb: visszaadja a nyers pointert, és a unique_ptr üres lesz — a felelősség a hívóé. Ha a hívó nem tudja biztosan, hogy a címzett felszabadítja, memóriaszivárgás lesz belőle, tipikusan úgy, hogy ptr.release() eredményét eldobjuk. A release() helyes használata az az eset, amikor egy C API vesz át tulajdonlást, és a dokumentációja ezt kimondja. A reset() ezzel szemben biztonságos: előbb felszabadít, aztán átveszi az újat.

21.3.5 Custom deleter: nem memóriajellegű erőforrás

A std::default_delete<T> egyszerűen delete-et hív, ami nem minden erőforrásra jó. A FILE*, a DIR*, a sqlite3*, egy socket, egy mmap-elt régió vagy egy dlopen handle nem delete-tel szabadul fel. A deleter ezt a rést tölti be.

// file_owner.cpp — C++20, teljes program
#include <cstdio>
#include <memory>
#include <string>
#include <utility>

struct FileCloser {
    void operator()(std::FILE* f) const noexcept {
        if (f != nullptr) { std::fclose(f); }
    }
};
using FilePtr = std::unique_ptr<std::FILE, FileCloser>;

enum class ReadResult { line, eof, error };

class LineSource {
public:
    explicit LineSource(FilePtr in) : in_(std::move(in)) {}   // owner átvétele

    // Nem tulajdonló kimenet: a hívó birtokolja a buffert.
    // line: out egy teljes (vagy az utolsó, LF nélküli) sor;
    // eof:  nincs több adat, out üres;
    // error: olvasási hiba; out tartalma (részleges sor) nem érvényes rekord.
    ReadResult next(std::string& out) {
        out.clear();
        for (int c = std::fgetc(in_.get()); c != EOF; c = std::fgetc(in_.get())) {
            if (c == '\n') { return ReadResult::line; }
            out.push_back(static_cast<char>(c));
        }
        if (std::ferror(in_.get())) { return ReadResult::error; }   // EOF ≠ fájlvég
        return out.empty() ? ReadResult::eof : ReadResult::line;
    }
private:
    FilePtr in_;   // owner: a fájl élettartama ehhez az objektumhoz kötött
};

FilePtr open_file(const char* path) {
    return FilePtr(std::fopen(path, "rb"));   // nullptr → üres FilePtr, legális
}

int main(int argc, char** argv) {
    if (argc != 2) {
        std::fprintf(stderr, "usage: file_owner PATH\n");
        return 2;
    }
    FilePtr f = open_file(argv[1]);
    if (!f) {                       // üres owner: a megnyitás sikertelen
        std::perror(argv[1]);
        return 1;
    }
    LineSource source(std::move(f));  // az owner átkerül a LineSource-ba
    std::string line;                 // a hívó birtokolja a puffert
    std::size_t count = 0;
    ReadResult r;
    while ((r = source.next(line)) == ReadResult::line) {
        ++count;
    }
    if (r == ReadResult::error) {
        std::fprintf(stderr, "%s: read error after %zu lines\n", argv[1], count);
        return 3;
    }
    std::printf("lines=%zu\n", count);
}

A példa szerződései: a LineSource az egyetlen owner; a next() std::string& paramétere borrowed, a hívó felel érte; a open_file sikertelenséget üres FilePtr-rel jelez, nem kivétellel, így a hívónak if (!f) { ... } ellenőrzést kell tennie. A next() háromféle eredményt ad, mert az fgetc EOF visszatérése kétféle eseményt takar: fájlvéget vagy olvasási hibát. A kettőt csak az ferror választja szét. Fájlvégnél az utolsó, LF nélküli sor még szabályos line, utána eof jön; hibánál error, és az out-ban maradt részleges sor nem válik csendben sikeres rekorddá. A FileCloser::operator() noexcept: a destruktorból hívott deleter nem dobhat, különben std::terminate lenne a következmény. Ez a lezárásra vonatkozó szerződés, és nem tiltja, hogy a next() hibát jelezzen — eredménykóddal, ahogy itt, vagy akár kivétellel.

A main driver a teljes programhoz tartozik: argumentumot ellenőriz, az üres ownert hibaként kezeli, megszámolja a sorokat, és olvasási hibánál 3-mal lép ki. Build és diagnosztikai parancs:

g++ -std=c++20 -Wall -Wextra -Wpedantic -g -O1 \
    -fsanitize=address,undefined file_owner.cpp -o file_owner
printf 'a\nb\nc' > three.txt      # utolsó sor LF nélkül
printf 'a\nb\nc\n' > three_lf.txt
: > empty.txt
./file_owner three.txt; ./file_owner three_lf.txt; ./file_owner empty.txt
mkdir -p adir; ./file_owner adir   # Linux: fopen sikerül, az olvasás EISDIR

Buildjegyzőkönyv (lab-gcc profil). A program figyelmeztetés nélkül fordult; a three.txt és a three_lf.txt fájlra lines=3, az üres fájlra lines=0 kimenettel, 0 kóddal futott, ASan/UBSan hibajelzés nélkül. A könyvtárra adir: read error after 0 lines üzenettel 3-mal lépett ki; ez glibc/Linux-specifikus reprodukció (a fopen könyvtárra is sikerül, az első olvasás EISDIR hibát ad), nem hordozható teszt. Nem létező fájlra a perror üzenetét írja ki, és 1-gyel lép ki; argumentum nélkül a használati sort írja ki, és 2-vel lép ki.

A hibaágat a sor elején és egy részleges sor után is célzottan érdemes kipróbálni. Ehhez a glibc fopencookie függvényével olyan FILE*-ot építünk, amely előbb adatot ad, majd EIO hibát; a line_source.hpp a fenti program main nélküli része:

// read_error_probe.cpp — C++20 + glibc fopencookie: injektált olvasási hiba
#include "line_source.hpp"
#include <cassert>
#include <cerrno>
#include <cstring>
#include <sys/types.h>

struct Script { const char* data; bool sent; };

static ssize_t fail_read(void* cookie, char* buf, size_t size) {
    auto* s = static_cast<Script*>(cookie);
    if (!s->sent && s->data[0] != '\0') {          // első hívás: adat
        std::size_t n = std::strlen(s->data);
        if (n > size) n = size;
        std::memcpy(buf, s->data, n);
        s->sent = true;
        return static_cast<ssize_t>(n);
    }
    errno = EIO;                                    // utána: I/O hiba
    return -1;
}

static ReadResult first_after(const char* data, int skip, std::string& out) {
    static Script s; s = Script{data, false};
    cookie_io_functions_t io{fail_read, nullptr, nullptr, nullptr};
    LineSource src(FilePtr(fopencookie(&s, "r", io)));
    ReadResult r = ReadResult::line;
    for (int i = 0; i <= skip; ++i) r = src.next(out);
    return r;
}

int main() {
    std::string out;
    assert(first_after("", 0, out) == ReadResult::error);            // hiba a sor elején
    assert(first_after("ab\npartial", 0, out) == ReadResult::line && out == "ab");
    assert(first_after("ab\npartial", 1, out) == ReadResult::error); // részleges sor után
    std::puts("read_error_probe: OK");
}

Buildjegyzőkönyv (lab-gcc profil). g++ -std=c++20 -Wall -Wextra -Wpedantic -g -O1 -fsanitize=address,undefined read_error_probe.cpp figyelmeztetés nélkül fordult, a futás kimenete read_error_probe: OK: üres adat után azonnali hibára error, az ab\npartial adat után az első hívás line (ab), a második — a partial részleges sor után érkező hibára — error. A fopencookie GNU-kiterjesztés; más platformon ugyanez a próba egy saját olvasási interfész mögé tett hibainjektáló forrással végezhető el.

21.3.5.1 A deleter és a sizeof implementációfüggése

A unique_ptr mérete nem szabványosított. A gyakorlatban az állapot nélküli (empty) deleterekre a fordítók üres bázis-optimalizálást (EBO) alkalmaznak, így a sizeof(std::unique_ptr<T>) jellemzően egy pointer méretű; függvénypointer-deleterrel (decltype(&std::fclose)) jellemzően két pointer; állapotot hordozó deleterrel ennél is több. Ezek implementációs megfigyelések, nem hordozható garanciák: ABI-stabil bináris interfészben ne építsünk rájuk, és ne tegyük fel, hogy sizeof(unique_ptr<T>) == sizeof(T*). Ha egy konkrét laborprofilban mégis számít, mérjük és rögzítsük static_assert-tel, a target ABI és a standard library kiadásának megnevezésével.

Ugyanide tartozik, hogy a unique_ptr<T, D> csak akkor default-konstruálható, ha a D is az. Egy capture-t tartalmazó lambda nem default-konstruálható, így az ilyen deleterrel a unique_ptr is csak deleter megadásával hozható létre — ez gyakran meglepi a kezdőt.

21.3.6 Incomplete type és a destruktor helye

A unique_ptr<T> deklarációjához nem kell, hogy a T teljes típus legyen; a destruktorához, a reset()-hez és a deleter hívásához viszont igen, mert ezeknél sizeof(T)-re és a T destruktorára van szükség. Ez a pimpl (pointer to implementation) minta alapja:

// widget.hpp: a Widget::Impl itt még incomplete
#include <memory>
class Widget {
public:
    Widget();
    ~Widget();                              // deklarálva, nem = default itt!
    Widget(Widget&&) noexcept;
    Widget& operator=(Widget&&) noexcept;
private:
    struct Impl;
    std::unique_ptr<Impl> impl_;
};

// widget.cpp: itt válik teljessé az Impl
#include "widget.hpp"
#include <vector>
struct Widget::Impl { std::vector<int> data; };
Widget::Widget() : impl_(std::make_unique<Impl>()) {}
Widget::~Widget() = default;                // a definíció helyén teljes az Impl
Widget::Widget(Widget&&) noexcept = default;
Widget& Widget::operator=(Widget&&) noexcept = default;

Gyakori hiba: a destruktort a headerben = default-tal definiálni. Ekkor a destruktor a headerben, az Impl teljessé válása előtt példányosul, és a fordítás „invalid application of sizeof to incomplete type” hibával áll meg. Ugyanez a helyzet a move műveletekkel: ha a headerrben = default-ot írunk, az implicit definíció ott példányosul, ahol az Impl még hiányos.

21.3.7 Owner és borrowed observer az API-ban

Az egyedi tulajdonlás akkor működik, ha az interfész egyértelműen elárulja, ki a felelős. Négy jellegzetes szignatúra:

Szignatúra Jelentés
void sink(std::unique_ptr<T> p) átveszi a tulajdonlást; a hívó után üres pointert kap vissza
void sink(std::unique_ptr<T>&& p) ugyanaz, &&- jelöléssel; nem fogad el lvalue-t
void borrow(const T& x) / void borrow(T* x) nem tulajdonol; csak az owner élettartama alatt érvényes
std::unique_ptr<T> make() új ownert ad vissza (move-pal)

A T* akkor jó, ha a hiányzó érték is megengedett (nullptr); ha nem, a T& kifejezőbb. Egyik sem él túl egy reset()-et, move-ot vagy a owner megszűnését — ezt a hívási szerződésben kell rögzíteni, mert a fordító nem fogja.

Kivétel, amit érdemes fejben tartani: a unique_ptr<Derived> nem konvertálható automatikusan unique_ptr<Base>-re, ha a default_delete<Base> nem tudja fogadni a Derived*-ot; a Base-nek tehát virtuális destruktor kell (lásd Virtuális destruktor, slicing és ownership (24.3)). A unique_ptr<Base>(new Derived) alak ráadásul a make_unique védelmét elveszti, ezért ilyenkor unique_ptr<Base>-t make_unique<Derived>()-ből, explicit módon kell építeni.

21.3.8 Tipikus hibák és javításuk

  1. Tárolt get() eredmény. const T* p = owner.get(); majd owner.reset(...) — a p dangling. Javítás: az observer élettartamát az owner élettartamához kötni, vagy weak_ptr-t használni (lásd shared_ptr, control block és weak_ptr (21.4)) ott, ahol a hosszabb életű megfigyelés valóban igény.
  2. release() a reset() helyett. p.release(); p.reset(new T); — az első sor elszivárogtatja a régit. Javítás: p.reset(...).
  3. new + unique_ptr konstrukció szétbontva. Kivétel esetén szivárgás. Javítás: make_unique.
  4. Incomplete type a headerben defaultolt destruktorral. Javítás: a destruktor és a move műveletek deklarációja a headerben, definíciója a .cpp-ben.
  5. Hossz nélküli unique_ptr<T[]>. A tömb mérete nem kérdezhető le; a hosszt külön kel vinni. Javítás: unique_ptr<T[]> + méret, vagy std::vector<T>.

21.3.9 Feladatok

1. feladat (értelmezés). Az alábbi szignatúrák közül melyik ad át tulajdonlást, melyik csak kölcsönöz? Indokolj. (a) void f(std::unique_ptr<Conn>), (b) void g(const Conn&), (c) std::unique_ptr<Conn> h(), (d) void i(std::unique_ptr<Conn>&), (e) void j(Conn*).

2. feladat (kódolás). Készíts FdPtr típust POSIX fájlleíróhoz struct FdCloser deleterrel, ::close-t hívva. Írj FdPtr open_fd(const char*) függvényt, és egy Duplex osztályt, amely két FdPtr-t birtokol, mozgatható, és a read/write irányt külön tárolja. Add meg a buildparancsot.

3. feladat (diagnosztika). Az alábbi kód lefordul, de futásidőben hibás. Nevezd meg a hibát, a dangling observer élettartam-határát és a javítást.

class Session {
public:
    explicit Session(std::unique_ptr<Conn> c) : conn_(std::move(c)) {}
    const Conn* conn() const { return conn_.get(); }
    void reconnect(std::unique_ptr<Conn> c) { conn_ = std::move(c); }
private:
    std::unique_ptr<Conn> conn_;
};

auto s = std::make_unique<Session>(std::make_unique<Conn>());
const Conn* c = s->conn();                 // borrowed observer
s->reconnect(std::make_unique<Conn>());    // invalidálja c-t
inspect(*c);                               // ?

21.3.10 Megoldások

1. megoldás. (a) A paraméter érték szerinti unique_ptr, tehát a hívó move-pal adja át, és a hívás után a saját változója üres lesz — tulajdonlásátadás, „sink”. (b) const Conn&: nem tulajdonol, csak a hívás idejére érvényes, a hívó felel az élettartamért. (c) Visszatérési értékként unique_ptr: új ownert ad, a hívó veszi át (a move-t a visszatérési érték biztosítja). (d) unique_ptr<Conn>&: nem ad át tulajdonlást, hanem újraültetheti a hívó objektumát — out-paraméter, amely a hívó ownerét módosítja; ez nem sink, mert a hívó unique_ptr-je tovább él. (e) Conn*: nem tulajdonol; nullable observer, élettartama a hívó felelőssége. Ellenőrzési feltétel: minden válasznál nevezd meg, ki a owner a hívás után.

2. megoldás.

// fd_owner.cpp — C++20, Linux/POSIX
#include <unistd.h>
#include <cerrno>
#include <memory>
#include <utility>

struct FdCloser {
    void operator()(int* p) const noexcept {
        if (p != nullptr) { ::close(*p); delete p; }
    }
};
using FdPtr = std::unique_ptr<int, FdCloser>;

FdPtr adopt_fd(int fd) {
    if (fd < 0) { return FdPtr(nullptr); }
    return FdPtr(new int(fd));      // a leírt értéket a heapen tároljuk
}

class Duplex {
public:
    Duplex(FdPtr in, FdPtr out) : in_(std::move(in)), out_(std::move(out)) {}
    Duplex(Duplex&&) noexcept = default;
    Duplex& operator=(Duplex&&) noexcept = default;
    Duplex(const Duplex&) = delete;
    Duplex& operator=(const Duplex&) = delete;

    int in_fd()  const { return in_  ? *in_  : -1; }   // borrowed: nem ad át ownert
    int out_fd() const { return out_ ? *out_ : -1; }
    bool valid() const { return static_cast<bool>(in_) && static_cast<bool>(out_); }
private:
    FdPtr in_, out_;
};

Indoklás: a unique_ptr deletere pointert kap, ezért a int leírót heapen kell tárolni; a FdCloser állapot nélküli, így a unique_ptr mérete jellemzően egy pointer marad (ezt konkrét laborprofilban ellenőrizd, ne feltételezd). A másolás törlése és a move noexcept-ká tétele azért kell, hogy a Duplex konténerbe tehető legyen (lásd noexcept, swap és container-reallocation (22.5)). A close() hibáját a deleter nem tudja jelenteni — ez tudatos tervezési döntés; ha számít, a lezárást explicit close_checked() metódusban kell kezelni a reset() előtt. Build: g++ -std=c++20 -Wall -Wextra -Wpedantic -g -fsanitize=address,undefined fd_owner.cpp -o fd_owner. Ellenőrzési feltétel: Duplex move-ja után a forrás valid() értéke false, és a mozgatott Duplex destruktora pontosan két close-t vált ki.

3. megoldás. A hiba nem a unique_ptr-ben van, hanem a conn() által visszaadott borrowed observerben: a const Conn* c élettartam-határa a reconnect() hívás (pontosabban az a pont, ahol a conn_ régi értéke reset-elődik), nem a Session élettartama. A reconnect() után a c dangling pointer, az inspect(*c) nem definiált viselkedés — a tünet lehet látszólag helyes működés, ASan-hiba vagy összeomlás, garantált kimenet nincs. Javítási lehetőségek: (i) az observer használatát a reconnect() elé rendezni, és az élettartamot kommentben/documentációban rögzíteni; (ii) ha hosszabb életű megfigyelés kell, std::weak_ptr-t használni a Conn-ra (ez shared_ptr tulajdonlást igényel, lásd shared_ptr, control block és weak_ptr (21.4)); (iii) az inspect-et a Session egy tagfüggvényévé tenni, amely mindig a conn_ aktuális értékét használja, és nem enged kívülre pointert. Ellenőrzési feltétel: a javított változatban az observer soha nem él túl egy reset/move/reconnect műveletet, és ezt a fejléc kommentje kimondja.

21.3.11 Összefoglaló

A std::unique_ptr a C-s tulajdonlási konvenciót típusba öntött, move-only szerződése: pontosan egy owner, determinisztikus felszabadítás, kivételbiztos konstrukció make_unique-jal. Az objektumtömb és az egyedi objektum két külön specializáció, eltérő deleterrel és operátorokkal. A get() kölcsönöz, a reset() újraültet, a release() átadja a felelősséget — ez utóbbi csak indokolt FFI-határon használható. A custom deleter nem memóriajellegű erőforrásokat (FILE*, fd, handle) tesz szabályossá, de a unique_ptr mérete implementációfüggő, ezért ABI-határon nem építhetünk rá. Incomplete type esetén a destruktort és a move műveleteket a headerben csak deklarálni szabad, definiálni a teljes típus mellett kell. Végül: a unique_ptr a tulajdonlást rendezi, az observer élettartamát nem — a get() eredménye ugyanolyan borrowed, mint bármely nyers pointer.