22.1 Rule of zero, three és five

A C-ben egy struct értékadása és paraméterátadása byte-onkénti másolás. Ha a struktúra mutatót tartalmaz egy dinamikusan allokált erőforrásra, a másolat ugyanarra a címre mutat: két „tulajdonos” jön létre ugyanarra az erőforrásra. A következmény vagy double free, vagy use-after-free. Ez a shallow copy (sekély másolás) probléma, amelyet a C típusrendszere nem tud kifejezni.

/* shallow_copy.c - szemléltető példa; futtatása nem definiált viselkedéshez vezet */
#include <stdlib.h>

struct buffer {
    char *data;
    size_t len;
};

int main(void) {
    struct buffer a;
    a.len = 4;
    a.data = malloc(a.len);
    if (a.data == NULL) return 1;

    struct buffer b = a;   /* sekély másolat: b.data == a.data */
    free(b.data);
    free(a.data);          /* double free: nem definiált viselkedés */
    return 0;
}
gcc -std=c17 -Wall -Wextra -Wpedantic shallow_copy.c -o shallow_copy
# A példa szándékosan hibás; futtatni nem javasolt.

A C++ értékszemantikája nem tünteti el a problémát, hanem kifejezhetővé teszi: a special member functions (különleges tagfüggvények) adják meg, hogy egy típus hogyan másolható, mozgatható és semmisíthető meg. Ez az alfejezet a generálási szabályokat, a rule of three/five/zero döntési keretét és a copy/move policy megválasztását tárgyalja. A std::move mint kategóriakonverzió és a moved-from állapot részletes tárgyalása a Move constructor, moved-from állapot és std::move része; itt csak a special memberek szerepéig jutunk el. Az owner/view szerződésre építünk az Owner, view és erőforrásszerződés C++ API-ban, az invariáns fogalmára az Osztály, konstruktor és invariáns, a C értékmásolásra a Struktúrák, tagok és értékmásolás résznél. A szabályok forrása a forrásjegyzék [C++20] tétele: [class.default.ctor], [class.copy.ctor], [class.copy.assign], [class.dtor] és [dcl.fct.def.delete]; a táblázatot alább egy próbaprogrammal ellenőrizzük.

22.1.1 A special member functions készlete

C++20-ban hat special member function van: default constructor, destructor, copy constructor, copy assignment operator, move constructor, move assignment operator. Ezeket a compiler akkor deklarálja implicit módon, ha a program nem deklarálja őket. A deklaráció azonban nem garantál definíciót: a compiler bizonyos esetekben „defined as deleted” (töröltként definiált) állapotba teszi őket, más esetekben egyáltalán nem deklarálja (suppressed, elnyomott).

Pontos definíció: egy special member function akkor „implicit módon deklarált”, ha a felhasználó nem deklarálta; akkor „defined as deleted”, ha a generált definíció nem lenne érvényes (például egy tag nem másolható); és akkor „nem deklarált”, ha egy másik user-declared special member ezt kizárja. A három állapot megkülönböztetése kulcs: a törölt függvényre történő hívás fordítási hibát ad, míg a nem deklarált függvény helyett a túlterhelés-feloldás másikat, jellemzően a copy-t választja.

A generálási szabályokat a következő táblázat foglalja össze. Jelölések: I = implicit módon deklarált és definiált, ha a tagok engedik; U = user-declared; D = defined as deleted; – = nem deklarált.

User-declared default ctor destructor copy ctor copy assign move ctor move assign
semmi I I I I I I
default ctor U I I I I I
destructor I U I* I* – –
copy ctor – I U I* – –
copy assign I I I* U – –
move ctor – I D D U –
move assign I I D D – U

A csillaggal jelölt esetben az implicit copy művelet generálása deprecated: az implicit copy constructoré, ha az osztálynak user-declared copy assignmentje vagy destruktora van; az implicit copy assignmenté, ha user-declared copy constructora vagy destruktora van. Ez nem tiltja meg a generálást, de a szabvány a jövőbeni eltávolítás lehetőségét fenntartja; új kódban ne támaszkodjunk rá.

A táblázat három mintázata. Először: a user-declared destructor, copy ctor vagy copy assign elnyomja a move operations deklarációját, ilyenkor a move helyett a copy hívódik, ha elérhető. Másodszor: a user-declared move ctor vagy move assign törli a copy operations-t, ez teszi lehetővé a move-only típust. Harmadszor: a default ctor csak akkor generált, ha nincs semmilyen user-declared konstruktor, ezért a copy ctor és a move ctor sorában is „–” áll; a copy assignment vagy a move assignment viszont nem konstruktor, mellettük a default ctor megmarad. A többi special member generálását a default ctor nem befolyásolja.

A táblázatot minimáltípusokkal ellenőrizhetjük. Minden típus pontosan egy special membert deklarál, egy naplózó Probe tagon keresztül pedig az is látszik, hogy rvalue argumentumnál move vagy copy fut, illetve törölt-e a művelet (D):

// special_members.cpp — C++20: a generálási táblázat ellenőrzése
#include <cstdio>
#include <type_traits>
#include <utility>

struct Probe {                      // naplózza, melyik műveletét hívták
    static inline const char* last = "";
    Probe() = default;
    Probe(const Probe&) { last = "copy"; }
    Probe(Probe&&) noexcept { last = "move"; }
    Probe& operator=(const Probe&) { last = "copy"; return *this; }
    Probe& operator=(Probe&&) noexcept { last = "move"; return *this; }
};

// Soronként egy user-declared special member; az int-es konstruktor csak
// a példányosításhoz kell, és a default ctoron kívül semmit nem befolyásol.
struct None       { Probe p; None() = default; explicit None(int) {} };
struct DefCtor    { Probe p; DefCtor() {} explicit DefCtor(int) {} };
struct Dtor       { Probe p; ~Dtor() {} };
struct CopyCtor   { Probe p; explicit CopyCtor(int) {}
                    CopyCtor(const CopyCtor& o) : p(o.p) {} };
struct CopyAssign { Probe p;
                    CopyAssign& operator=(const CopyAssign& o) { p = o.p; return *this; } };
struct MoveCtor   { Probe p; explicit MoveCtor(int) {}
                    MoveCtor(MoveCtor&& o) noexcept : p(std::move(o.p)) {} };
struct MoveAssign { Probe p;
                    MoveAssign& operator=(MoveAssign&& o) noexcept { p = std::move(o.p); return *this; } };

template <class T>
const char* via_move_ctor() {        // melyik konstruktor fut rvalue-ból?
    if constexpr (!std::is_move_constructible_v<T>) return "D";
    else {
        T a{1};
        Probe::last = "";
        T b(std::move(a));
        return Probe::last;
    }
}

template <class T>
const char* via_move_assign() {
    if constexpr (!std::is_move_assignable_v<T>) return "D";
    else {
        T a{1}, b{1};
        Probe::last = "";
        b = std::move(a);
        return Probe::last;
    }
}

template <class T>
void row(const char* name) {
    std::printf("%-11s default=%d copy_ctor=%d copy_assign=%d "
                "rvalue_ctor=%s rvalue_assign=%s\n", name,
                std::is_default_constructible_v<T>,
                std::is_copy_constructible_v<T>, std::is_copy_assignable_v<T>,
                via_move_ctor<T>(), via_move_assign<T>());
}

// Az int-es konstruktor nélküli típusokhoz aggregát/default példányosítás.
template <> const char* via_move_ctor<Dtor>() { Dtor a; Probe::last = ""; Dtor b(std::move(a)); return Probe::last; }
template <> const char* via_move_assign<Dtor>() { Dtor a, b; Probe::last = ""; b = std::move(a); return Probe::last; }
template <> const char* via_move_ctor<CopyAssign>() { CopyAssign a; Probe::last = ""; CopyAssign b(std::move(a)); return Probe::last; }
template <> const char* via_move_assign<CopyAssign>() { CopyAssign a, b; Probe::last = ""; b = std::move(a); return Probe::last; }
template <> const char* via_move_assign<MoveAssign>() { MoveAssign a, b; Probe::last = ""; b = std::move(a); return Probe::last; }

int main() {
    row<None>("semmi");
    row<DefCtor>("default");
    row<Dtor>("destructor");
    row<CopyCtor>("copy ctor");
    row<CopyAssign>("copy assign");
    row<MoveCtor>("move ctor");
    row<MoveAssign>("move assign");
}
g++ -std=c++20 -Wall -Wextra -Wpedantic -Wdeprecated-copy-dtor \
    special_members.cpp -o special_members
./special_members

Buildjegyzőkönyv (lab-gcc profil). A kimenet soronként (default, copy_ctor, copy_assign, rvalue-konstrukció, rvalue-értékadás): semmi 1 1 1 move move; default 1 1 1 move move; destructor 1 1 1 copy copy; copy ctor 0 1 1 copy copy; copy assign 1 1 1 copy copy; move ctor 0 0 0 move D; move assign 1 0 0 D move. Ahol az rvalue-művelet copy, ott a move nincs deklarálva (–), és a túlterhelés-feloldás a copy-t választja; ahol D, ott a move nincs deklarálva, a copy pedig törölt. A fordító négy elavultsági figyelmeztetést ad, pontosan a csillaggal jelölt cellákra.

A = default és = delete szintaxis:

class X {
public:
    X() = default;                          // kérjük a generált default ctort
    X(const X&) = delete;                   // copy tiltása
    X& operator=(const X&) = delete;
    ~X() = default;
};

A = default akkor hasznos, ha a special member-t deklarálni akarjuk, például virtual destructor miatt, de a definíciót a compilerre bízzuk. A = delete bármely függvényre alkalmazható, de nem veszi ki a jelöltet a túlterhelés-feloldásból: a törölt függvény ugyanúgy jelölt, ugyanúgy rangsorolódik, és ha ő nyer, a program hibás ([dcl.fct.def.delete]). Ezért alkalmas tiltott konverziók kifejezésére: a törölt overload „elkapja” azt a hívást, amely különben egy nem kívánt konverzióval egy másik overloadhoz jutna. Egyetlen szabványos kivétel van: a defaulted, de töröltként definiált move constructort és move assignmentet a túlterhelés-feloldás figyelmen kívül hagyja ([class.copy.ctor], [class.copy.assign]), így ilyenkor a copy-változat futhat. Ettől különbözik a constraint vagy SFINAE miatt ki sem választott jelölt, amely a túlterhelés-feloldásban részt sem vesz, így a hívás egy másik jelöltre eshet:

// deleted_overload.cpp — C++20: a deleted jelölt részt vesz a kiválasztásban
#include <concepts>
#include <cstdio>

void f(long) { std::puts("f(long)"); }
void f(int) = delete;              // jelölt marad; ha nyer, a hívás hibás

void g(double) { std::puts("g(double)"); }
template <std::integral T>
void g(T) { std::puts("g(integral)"); }   // nem integrálisra ki sem jelölt

int main() {
    f(1L);          // f(long): exact match, a deleted f(int) veszít
    // f(1);        // HIBA: az f(int) exact match nyer, de deleted
    g(1);           // g(integral)
    g(1.5);         // g(double): a sablon a constraint miatt nem életképes
}

Buildjegyzőkönyv (lab-gcc profil). A program f(long), g(integral), g(double) kimenettel fut; a kikommentezett f(1) visszakapcsolva use of deleted function 'void f(int)' fordítási hibát ad.

22.1.2 Rule of three és rule of five

A rule of three a C++11 előtti ökölszabály: ha egy osztálynak user-declared destructora, copy constructorja vagy copy assignment operatora van, akkor nagy valószínűséggel mindháromra szükség van. Az ok a fenti táblázatban látható: ha csak a destructort írjuk meg, a copy operations generáltak maradnak, és sekélyen másolnak. Ez pont a C-beli shallow copy hiba C++ változata.

A rule of five a C++11 utáni kiegészítés: ugyanez érvényes a move constructorra és a move assignment operatorra is. Ha az osztály erőforrást birtokol, és a move operations-t is definiáljuk, akkor öt függvényt kell összehangolni: destructor, copy ctor, copy assign, move ctor, move assign. A „rule of five” nem azt jelenti, hogy mindig mind az ötöt kézzel kell megírni; azt jelenti, hogy ha bármelyiket kézzel írjuk, végig kell gondolni a többit.

Nézzük a kézzel kezelt buffer rule-of-five változatát. A példa teljes, fordítható; a buildparancsot a blokk után adjuk.

// buffer_r5.cpp - rule of five, kézi erőforrás-kezelés
#include <cstddef>
#include <cstring>
#include <utility>

class Buffer {
public:
    explicit Buffer(std::size_t n = 0)
        : data_{n ? new char[n] : nullptr}, size_{n} {}

    ~Buffer() { delete[] data_; }

    Buffer(const Buffer& other)
        : data_{other.size_ ? new char[other.size_] : nullptr},
          size_{other.size_} {
        if (data_ != nullptr) {
            std::memcpy(data_, other.data_, size_);
        }
    }

    Buffer& operator=(const Buffer& other) {
        if (this != &other) {
            Buffer tmp{other};   // erős garancia: előbb elkészül az új érték
            swap(tmp);
        }
        return *this;
    }

    Buffer(Buffer&& other) noexcept
        : data_{other.data_}, size_{other.size_} {
        other.data_ = nullptr;
        other.size_ = 0;
    }

    Buffer& operator=(Buffer&& other) noexcept {
        if (this != &other) {
            delete[] data_;
            data_ = other.data_;
            size_ = other.size_;
            other.data_ = nullptr;
            other.size_ = 0;
        }
        return *this;
    }

    void swap(Buffer& other) noexcept {
        std::swap(data_, other.data_);
        std::swap(size_, other.size_);
    }

    std::size_t size() const noexcept { return size_; }
    const char* data() const noexcept { return data_; }

private:
    char* data_;
    std::size_t size_;
};
g++ -std=c++20 -Wall -Wextra -Wpedantic -fsanitize=address,undefined buffer_r5.cpp -o buffer_r5

A copy assignment copy-and-swapot használ: az other-ből előbb elkészül egy teljes másolat (tmp), és csak akkor cseréljük ki a régi állapotot, ha ez sikerült. Így ha a másolás közben kivétel dobódik, például a new sikertelen, a *this változatlan marad: ez az erős kivételgarancia (strong exception guarantee). A self-assignment (b = b) esetét a this != &other feltétel kezeli; copy-and-swap esetén ez elhagyható lenne, de a move assignmentnél kötelező, különben a saját erőforrásunkat szabadítanánk fel, majd vennénk át.

A move constructor és move assignment itt a tulajdonjog átadását végzi: a forrás pointerét átveszi, majd a forrást nullázza. A std::move önmagában csak kategóriakonverzió; a tényleges erőforrás-átadás a move operationsban történik. Ennek részletes tárgyalása a Move constructor, moved-from állapot és std::move részé.

22.1.3 Copy/move policy: másolható vagy move-only

Az erőforrás-tulajdonos osztály szerződése két alapvető irányt vehet. A másolható owner copy operationsa mély másolatot készít, move operationsa átadja a tulajdonjogot. Ezt akkor érdemes választani, ha az erőforrás másolása értelmes és megengedett, például egy buffer tartalma. A move-only owner copy operationsa törölt, move operationsa átadja a tulajdonjogot. Ezt akkor kell választani, ha az erőforrás egyedi, például file descriptor, socket, lock vagy egyedi allokáció, és két tulajdonos nem lehet.

A move-only szerződést a copy operations = delete jelölésével fejezzük ki. Az alábbi példa egy részlet: a UniqueFd osztály definíciója, amely POSIX fd-et birtokol.

#include <unistd.h>

class UniqueFd {
public:
    explicit UniqueFd(int fd = -1) noexcept : fd_{fd} {}
    ~UniqueFd() { if (fd_ >= 0) ::close(fd_); }

    UniqueFd(const UniqueFd&) = delete;
    UniqueFd& operator=(const UniqueFd&) = delete;

    UniqueFd(UniqueFd&& other) noexcept : fd_{other.fd_} {
        other.fd_ = -1;
    }
    UniqueFd& operator=(UniqueFd&& other) noexcept {
        if (this != &other) {
            if (fd_ >= 0) ::close(fd_);
            fd_ = other.fd_;
            other.fd_ = -1;
        }
        return *this;
    }

    int get() const noexcept { return fd_; }

private:
    int fd_;
};

Itt a copy törlése nem stílus kérdése: ha engednénk, két UniqueFd ugyanazt az fd-t zárná le, ami double close-t és más fd-ek véletlen lezárását okozhatja. A move-only szerződés a tulajdonjog egyediségét fejezi ki a típusban.

A self-assignment és a részleges hiba kezelése a move assignmentnél is kritikus. Ha this == &other, a régi fd lezárása után ugyanazt az fd-t vennénk át, ami már érvénytelen. Ezért kell a this != &other ellenőrzés. A copy assignmentnél a copy-and-swap adja az erős garanciát; a move assignmentnél a „delete then take” sorrend általában részleges hibát okozhat, ha a felszabadítás után az átvétel kivételt dobna. Mivel itt csak fd-t zárunk és int-et másolunk, ez nem fordulhat elő, de általános erőforrásnál a move assignmentet is érdemes úgy megírni, hogy előbb átvegyük az új erőforrást, és csak utána szabadítsuk fel a régit.

22.1.4 Rule of zero

A rule of zero a modern C++ irányvonal: ha lehetséges, egyik special member function-t se definiáljuk. Ezt akkor tehetjük meg, ha az osztály minden tagja maga kezeli az erőforrását, és a compiler generált copy/move operations helyesen működnek. A tagok lehetnek standard tárolók (std::vector, std::string), okos pointerek (std::unique_ptr, std::shared_ptr) vagy más RAII típusok.

A rule-of-zero Buffer:

// buffer_r0.cpp - rule of zero
#include <cstddef>
#include <vector>

class Buffer {
public:
    explicit Buffer(std::size_t n = 0) : data_(n) {}

    std::size_t size() const noexcept { return data_.size(); }
    char* data() noexcept { return data_.data(); }
    const char* data() const noexcept { return data_.data(); }

private:
    std::vector<char> data_;
};
g++ -std=c++20 -Wall -Wextra -Wpedantic buffer_r0.cpp -o buffer_r0

Itt nincs destructor, copy ctor, copy assignment, move ctor, move assignment. A std::vector destructora felszabadítja a tárolást, copy ctorja mély másolatot készít, move ctorja átadja a tárolást. A compiler generált special memberjei ezeket hívják, és helyesen működnek. A move-only változat ugyanígy működik std::unique_ptr<char[]> taggal: a unique_ptr nem másolható, ezért a Buffer copy operations implicit módon töröltek lesznek, a move operations pedig generáltak.

A rule of zero előnyei: kevesebb kézi kód és kevesebb hiba; a compiler generált műveletei optimalizálhatók, például trivial copy esetén; a tagok cseréje, például vector helyett unique_ptr, nem igényel kézi special member átírást; az invariáns a tagok invariánsaira épül, nem kell külön kézi erőforrás-kezelést összehangolni.

A rule of zero nem alkalmazható, ha az osztály invariánsa olyan, hogy a tagok másolása vagy mozgatása nem felel meg neki, például két tag konzisztenciáját kézzel kell fenntartani, vagy ha a tagok nem RAII-k. Ilyenkor a rule of three/five szerint kell eljárni.

A döntési sorrend: először vizsgáljuk meg, hogy minden tag tud-e önállóan viselkedni; ha igen, rule of zero. Ha nem, kell-e mély másolás; ha igen, rule of five, vagy rule of three, ha a move nem indokolt. Ha az erőforrás egyedi, move-only szerződés: = delete copy, move operations. Minden esetben ellenőrizni kell a self-assignmentet és a kivételgaranciát.

22.1.5 Tipikus hibák

  1. Csak destructor, copy nélkül. A copy operations generáltak maradnak, sekélyen másolnak; double free vagy use-after-free lesz a következmény. Javítás: rule of three/five vagy rule of zero.
  2. Rule of three hiányos. Van destructor és copy ctor, de nincs copy assignment; az assignment sekély marad. Javítás: mindhárom, vagy copy-and-swap.
  3. Move operations mellett elfelejtett copy törlés. Ha a move ctor user-declared, a copy operations automatikusan töröltek lesznek; ha ezt nem akarjuk, explicit = default-tal vissza kell hozni őket, ha a tagok engedik. Fordítva: ha copy-t írunk, a move-ok elnyomottak lesznek, és a move helyett copy hívódik.
  4. Self-assignment elfelejtése. a = a esetén a move assignment felszabadíthatja a saját erőforrását, majd érvénytelen pointert vesz át. Javítás: this != &other vagy copy-and-swap.
  5. = default nem másolható taggal. Ha a tag nem másolható, a = default copy ctor törölt lesz, nem generált; a hibaüzenet gyakran a hívás helyén jelenik meg. Javítás: a tag cseréje vagy a copy tiltása.
  6. Moved-from objektum használata. A move után a forrás érvényes, de nem specifikált állapotú; csak destruktálható vagy új érték adható neki. Részletek: Move constructor, moved-from állapot és std::move.

22.1.6 Feladatok

  1. Magyarázó. Adott:

    struct S {
        ~S();
        int x;
    };

    Mely special member functions generálódnak, törlődnek, illetve nem deklarálódnak? Másolható-e S, és mi történik a move kísérletnél?

  2. Kódolási. Alakítsd át a buffer_r5.cpp Buffer osztályát rule-of-zero változattá std::vector<char> használatával. Indokold, miért marad helyes a copy és a move.

  3. Diagnosztikai. Az alábbi osztály double free-t okoz. Miért, és hogyan javítod minimálisan?

    class Buf {
    public:
        explicit Buf(std::size_t n) : p_{new char[n]}, n_{n} {}
        ~Buf() { delete[] p_; }
    private:
        char* p_;
        std::size_t n_;
    };
  4. Tervezési. Írj move-only RAII wrappert egy POSIX file descriptor köré. A copy operations legyenek töröltek, a move operations adják át a tulajdonjogot, a destructor zárja le az fd-t. Miért nem szabad a copy-t engedélyezni?

22.1.7 Megoldások

1. megoldás. A user-declared destructor miatt a move constructor és a move assignment nem deklarált, azaz elnyomott. A copy constructor és a copy assignment implicit módon generált; a szabvány ezt deprecatednek jelöli, de működik. A default constructor generált, mert nincs más user-declared constructor. Az S tehát másolható; a std::move(s) egy S&&-t ad, de mivel nincs move ctor, a copy ctor hívódik. Ellenőrzési feltétel: std::is_copy_constructible_v<S> igaz, és a move kísérlet is lefordul, de a copy ctor fut.

2. megoldás. A Buffer tagja std::vector<char> lesz; a constructor data_(n), a size() és data() a vector megfelelő függvényeit hívja. Nem definiálunk destructort, copy/move operations-t. A vector copy ctorja mély másolatot készít, move ctorja átadja a tárolást; a compiler generált Buffer copy/move ezeket hívja. Ellenőrzési feltétel: a copy után a két buffer tartalma független; a move után a forrás size() értéke általában 0, de a vector moved-from állapota nem specifikált, csak érvényes.

3. megoldás. A Buf-nak csak destructora van, a copy ctor és copy assignment implicit generált, sekélyen másolja a p_ pointert. Buf b2 = b1; után b1.p_ == b2.p_, és mindkét destructor delete[]-eli ugyanazt a tömböt: double free, azaz nem definiált viselkedés. Minimális javítások: a copy operations törlése = delete jelöléssel, ha a Buf move-only owner; rule of five szerinti mély copy ctor és copy assignment, valamint move ctor és move assignment; vagy rule of zero std::vector<char> vagy std::unique_ptr<char[]> taggal. Ellenőrzési feltétel: a javított változat ASan-nel futtatva nem jelez double free-t; a copy után a két objektum külön tárolóra mutat.

4. megoldás. A UniqueFd osztály a fenti példa szerint definiálható. A copy-t azért nem szabad engedélyezni, mert az fd egyedi OS-erőforrás: két UniqueFd ugyanazt az fd-t zárná le, ami double close-t, más fd-ek véletlen lezárását és nehezen diagnosztizálható hibákat okoz. A move-only szerződés a tulajdonjog egyediségét fejezi ki. Ellenőrzési feltétel: a copy hívása fordítási hibát ad; a move után a forrás get() értéke -1, a cél a régi fd-t birtokolja. A megoldás részlet, a teljes programhoz main és a megfelelő include-ok szükségesek.

22.1.8 Összefoglaló

A C shallow copy problémáját a C++ special member functions teszik kifejezhetővé. A compiler hat special member functiont deklarálhat: default ctor, destructor, copy ctor, copy assignment, move ctor, move assignment. Ezek lehetnek implicit módon definiáltak, töröltek vagy elnyomottak; a user-declared destructor vagy copy elnyomja a move-okat, a user-declared move törli a copy-okat. A rule of three a destructor–copy ctor–copy assignment hármast, a rule of five a move operations-t is összehangolja. A rule of zero a modern alapértelmezés: RAII tagokkal a compiler generált műveletei helyesek. Az owner szerződése lehet másolható, mély copy-jal, vagy move-only, = delete copy-jal. Minden kézi assignmentnél kezelni kell a self-assignmentet és a részleges hibát: a copy-and-swap erős garanciát ad, a move assignmentnél a this != &other ellenőrzés kötelező. A részletes move-szemantika és a moved-from állapot a Move constructor, moved-from állapot és std::move, a copy elision a Copy elision, RVO/NRVO és értékvisszaadás, a noexcept és a container-reallocation a noexcept, swap és container-reallocation témája.