Čtení: 40 minHodnotí se: úlohy 1 a 2, zkouška

1 Objekty, úložiště a jejich doba existence

Po přečtení umíš
  • oddělit alokaci úložiště od vytvoření objektu a destrukci od dealokace
  • použít placement new, construct_at a destroy_at
  • zajistit správné zarovnání
  • vysvětlit, kdy objekt začíná a končí existovat

Toto je základ úloh 1 a 2. Stejný princip používá std::vector, std::optional i std::variant.

1.1 Ukázka: čtyři kroky místo jednoho

Výraz new std::string("ahoj") udělá dvě věci naráz: alokuje paměť a vytvoří objekt. Vektor je potřebuje oddělit, protože má místo na objekty, které ještě neexistují.

#include <cassert>
#include <new>
#include <string>

int main() {
  void* raw = ::operator new(sizeof(std::string));  // 1. úložiště, žádný objekt
  auto* text = new (raw) std::string("ahoj");       // 2. vytvoření objektu
  assert(*text == "ahoj");
  text->~basic_string();                            // 3. destrukce, úložiště zůstává
  ::operator delete(raw);                           // 4. dealokace
}
prvekprveksurová paměťsurová paměť
Velikost 2, kapacita 4. Objekty existují jen v prvních dvou místech, zbytek je surová paměť.
Co Spojeně Odděleně
vytvořit new T(...) operator new + placement new
zrušit delete p volání destruktoru + operator delete
pole new T[n] vytvoří nn objektů úložiště pro nn, objekty podle potřeby
Varování 1.2 (Každý krok jen jednou)

Dvojí destrukce i práce s objektem po destrukci jsou nedefinované chování. Totéž platí pro kombinaci new[] s delete: u typu s destruktorem si new[] ukládá počet prvků (cvičení 3).

1.2 Objekt a jeho doba existence

Definice 1.3 (Doba existence)

Objekt začíná existovat až po dokončení inicializace a končí začátkem destruktoru. Úložiště je jen paměť, kde objekt leží.

Tři důsledky, které se ptají často:

#include <cassert>
#include <string>
#include <vector>

std::vector<std::string> log_;

struct Part {
  std::string name;
  explicit Part(std::string n) : name(std::move(n)) { log_.push_back("+" + name); }
  ~Part() { log_.push_back("-" + name); }
};

struct Pair {
  Part first{"a"};
  Part second{"b"};
};

int main() {
  { Pair p; }
  assert((log_ == std::vector<std::string>{"+a", "+b", "-b", "-a"}));
}

1.3 Zarovnání

Každý typ má požadavek alignof(T). Objekt smí ležet jen na násobku této hodnoty. operator new vrací paměť zarovnanou pro běžné typy. Vyšší požadavek řeší varianta s std::align_val_t.

Pro úložiště uvnitř objektu slouží alignas:

#include <cstddef>

template <typename T>
struct Storage {
  alignas(T) std::byte bytes[sizeof(T)];
};

static_assert(alignof(Storage<double>) == alignof(double));
static_assert(sizeof(Storage<int>) == sizeof(int));

Kvůli zarovnání vzniká padding. Pořadí členů mění velikost třídy:

struct Bad { char a; int b; char c; };
struct Good { int b; char a; char c; };

static_assert(sizeof(Bad) == 12);
static_assert(sizeof(Good) == 8);

1.4 Paměťové algoritmy

Místo ručních smyček používej <memory>:

Funkce Co dělá
std::construct_at(p, args...) vytvoří objekt na adrese
std::destroy_at(p) zavolá destruktor, funguje i pro int
std::uninitialized_copy(b, e, out) vytvoří kopie v surové paměti
std::uninitialized_move(b, e, out) totéž přesunem
std::destroy(b, e) zruší rozsah
Poznámka 1.4 (Proč destroy_at)

V generickém kódu p->~T() funguje, ale destroy_at je čitelnější a pro pole zruší i prvky. Hlavně ale nemusíš psát název destruktoru, u std::string by to byl ~basic_string.

Největší výhoda: uninitialized_copy při výjimce sám zruší už vytvořené kopie.

#include <cassert>
#include <memory>
#include <new>
#include <stdexcept>

struct Fussy {
  static inline int alive = 0;
  int value;
  explicit Fussy(int v) : value(v) { ++alive; }
  Fussy(const Fussy& other) : value(other.value) {
    if (value == 3) throw std::runtime_error("třetí kopie selže");
    ++alive;
  }
  ~Fussy() { --alive; }
};

int main() {
  {
    Fussy source[3] = {Fussy(1), Fussy(2), Fussy(3)};
    assert(Fussy::alive == 3);
    void* raw = ::operator new(3 * sizeof(Fussy));
    auto* target = static_cast<Fussy*>(raw);
    try {
      std::uninitialized_copy(source, source + 3, target);
      assert(false);
    } catch (const std::runtime_error&) {
      assert(Fussy::alive == 3);  // kopie 1 a 2 algoritmus sám zrušil
    }
    ::operator delete(raw);
  }
  assert(Fussy::alive == 0);
}

1.5 Vzor: vlastní optional

Přednáška staví tento princip na std::optional. Úložiště je přímo v objektu a hodnota vzniká až na požádání.

#include <cassert>
#include <cstddef>
#include <memory>
#include <new>
#include <string>

template <typename T>
class Maybe {
  alignas(T) std::byte storage_[sizeof(T)];
  bool has_ = false;
  T* slot() { return std::launder(reinterpret_cast<T*>(storage_)); }

public:
  Maybe() = default;
  Maybe(const Maybe&) = delete;
  Maybe& operator=(const Maybe&) = delete;
  ~Maybe() { reset(); }

  void set(const T& value) {
    reset();
    std::construct_at(reinterpret_cast<T*>(storage_), value);
    has_ = true;
  }
  void reset() {
    if (has_) std::destroy_at(slot());
    has_ = false;
  }
  bool has_value() const { return has_; }
  T& value() { return *slot(); }
};

int main() {
  Maybe<std::string> m;
  assert(!m.has_value());
  m.set("dlouhý řetězec, který se nevejde do interního bufferu");
  assert(m.value().size() > 20);
  m.reset();
  assert(!m.has_value());
}

Sleduj příznak has_: bez něj by destruktor rušil objekt, který neexistuje.

1.6 Typické chyby

1.7 Kvíz

Kvíz

1. Co udělá ::operator new(100)?
2. Konstruktor vyhodí výjimku. Zavolá se destruktor téhož objektu?
3. V jakém pořadí se inicializují členské proměnné?
4. Jaké je sizeof struktury { char a; int b; char c; } na x86-64?
5. Co platí pro std::uninitialized_copy? Může být správně více možností, ale také žádná.
6. Proč nejde vektor napsat přes new T[capacity]?
Ukázat řešeníVytvořilo by to všech capacity objektů. Typ by musel být konstruovatelný výchozím způsobem, v kapacitě by ležely objekty, které nejsou prvky, a vkládání by vyžadovalo přiřazení.

1.8 Příklady na procvičení

Cvičení 1.5

Proč nejde kombinovat new T[n] a delete?

Řešení

U typu s netriviálním destruktorem si new[] uloží počet prvků před pole a delete[] ho čte, aby zrušil všechny. Obyčejné delete zruší jeden objekt a předá alokátoru jinou adresu.

Cvičení 1.6

Co je špatně v kódu T* p = static_cast<T*>(::operator new(sizeof(T))); *p = value;?

Řešení

Na adrese p žádný objekt není, takže přiřazení pracuje s neexistujícím objektem. Použij std::construct_at(p, value).

1.9 Tahák

Tahák
  • Alokace ≠ vytvoření. Destrukce ≠ dealokace.
  • operator new → construct_at → destroy_at → operator delete.
  • Objekt žije od konce inicializace po začátek destruktoru.
  • Konstruktor s výjimkou: bez destruktoru, podobjekty se ruší.
  • Podobjekty v pořadí deklarace, ruší se opačně.
  • Úložiště v objektu: alignas(T) std::byte b[sizeof(T)].
  • uninitialized_copy po sobě při výjimce uklidí.

Zdroje

  • ni_epc_02: objekt strany 4–5, úložiště 6–22, zarovnání 13–20, spojené a oddělené vytvoření 24–40, destrukce 41–48
  • ni_epc_03: paměťové algoritmy strany 3–17, podobjekty a padding 23–30, inicializační seznam 31–36, doba existence 39–47
  • NI-EPC cvičení 3, úloha 1