1 Objekty, úložiště a jejich doba existence
- oddělit alokaci úložiště od vytvoření objektu a destrukci od dealokace
- použít placement new,
construct_atadestroy_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
}
| 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ří objektů |
úložiště pro , objekty podle potřeby |
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
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:
- Pokud konstruktor vyhodí výjimku, objekt nikdy nezačal existovat a jeho destruktor se nezavolá. Už vytvořené podobjekty se ale zruší.
- Podobjekty se vytvářejí v pořadí deklarace, ne v pořadí v inicializačním seznamu.
- Destrukce probíhá v opačném pořadí.
#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 |
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
- Destruktor na prvek, který nikdy nevznikl.
memcpynetriviálních objektů místo konstrukce.- Úložiště bez
alignas. - Výjimka v půli vytváření prvků a únik úložiště.
- Záměna velikosti a kapacity.
1.7 Kvíz
1.8 Příklady na procvičení
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.
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
- 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_copypo sobě při výjimce uklidí.