2 Hodnoty, reference a přesun
- určit hodnotovou kategorii výrazu a co na něj jde navázat
- vysvětlit, co dělá
std::movea co ne - napsat správnou pětici speciálních funkcí, nebo poznat, že nejsou potřeba
- ošetřit sebe-přiřazení a moved-from stav
2.1 Ukázka: zbytečná kopie
Máme 10 MB dat v std::string a dáváme je do vektoru.
#include <cassert>
#include <string>
#include <utility>
#include <vector>
int main() {
std::vector<std::string> list;
std::string data(1000, 'x');
list.push_back(data); // kopie, data zůstanou
assert(data.size() == 1000);
list.push_back(std::move(data)); // přesun, obsah se jen převezme
assert(list.back().size() == 1000);
assert(data.size() <= 1000); // platný, ale nespecifikovaný stav
}
Rozdíl mezi řádky je jen v tom, jestli výraz říká „tento objekt ještě potřebuji“. O tom rozhoduje hodnotová kategorie.
2.2 Hodnotové kategorie
Každý výraz má typ a hodnotovou kategorii. Lhodnota označuje objekt, který existuje dál (x, *p, a[i]). Rhodnota označuje dočasnou hodnotu nebo objekt, jehož obsah už není potřeba (x + 1, f() vracející hodnotu, std::move(x)).
Pozor: pojmenovaná rhodnotová reference je výraz typu lhodnota.
#include <type_traits>
#include <utility>
int x = 0;
int&& r = std::move(x);
static_assert(std::is_lvalue_reference_v<decltype((x))>);
static_assert(std::is_lvalue_reference_v<decltype((r))>); // jméno je lhodnota
static_assert(std::is_rvalue_reference_v<decltype(std::move(x))>);
2.3 Co jde na co navázat
| Reference | lhodnota | rhodnota |
|---|---|---|
T& |
ano | ne |
const T& |
ano | ano |
T&& |
ne | ano |
const T& bere všechno, proto je to výchozí vstup funkce. Přetížení f(const T&) a f(T&&) pak rozliší kopii a přesun. Volá se to „lepší“: rhodnota jde na T&&.
2.4 Pět speciálních funkcí
Třída, která přímo spravuje zdroj, definuje destruktor, kopírovací konstruktor a přiřazení a přesouvací konstruktor a přiřazení. Třída složená z hotových typů nedefinuje nic.
#include <algorithm>
#include <cassert>
#include <cstddef>
#include <utility>
class Buffer {
std::size_t size_ = 0;
int* data_ = nullptr;
public:
Buffer() = default;
explicit Buffer(std::size_t n) : size_(n), data_(new int[n]{}) {}
~Buffer() { delete[] data_; }
Buffer(const Buffer& other) : Buffer(other.size_) {
std::copy(other.data_, other.data_ + size_, data_);
}
Buffer(Buffer&& other) noexcept
: size_(std::exchange(other.size_, 0)),
data_(std::exchange(other.data_, nullptr)) {}
Buffer& operator=(Buffer other) noexcept { // copy-and-swap: i kopie, i přesun
std::swap(size_, other.size_);
std::swap(data_, other.data_);
return *this;
}
std::size_t size() const { return size_; }
int& operator[](std::size_t i) { return data_[i]; }
};
int main() {
Buffer a(3);
a[0] = 7;
Buffer b = a;
Buffer c = std::move(a);
assert(b[0] == 7 && c[0] == 7 && a.size() == 0);
b = b;
b = std::move(c);
assert(b[0] == 7 && b.size() == 3);
}
Tři věci k zapamatování:
- Přesunový konstruktor je
noexcept, jinak hostd::vectorpři realokaci nepoužije. std::exchangepřevezme hodnotu a zdroj nechá prázdný, takže jeho destruktor nic nezruší.- Copy-and-swap řeší sebe-přiřazení i silnou záruku jedním tahem.
2.5 std::move nic nepřesouvá
std::move(x) jen převede x na rhodnotu. Přesun udělá až konstruktor nebo operátor, který se díky tomu vybere. U typu bez přesunu se vybere kopie.
#include <cassert>
#include <utility>
struct OnlyCopy {
int copies = 0;
OnlyCopy() = default;
OnlyCopy(const OnlyCopy& other) : copies(other.copies + 1) {}
};
int main() {
OnlyCopy a;
OnlyCopy b = std::move(a);
assert(b.copies == 1); // vybrala se kopie
}
Stejně tak const T se přesunout nedá: std::move z něj udělá const T&&, který se naváže na kopii.
2.6 Moved-from stav a sebe-přiřazení
Po přesunu je objekt platný, ale nespecifikovaný. Smíš ho zrušit a přiřadit do něj. v = std::move(v) musí nechat objekt v platném stavu, obsah se může ztratit.
Sebe-přiřazení při kopírování musí nechat objekt beze změny. U vlastních implementací pomůže test if (this != &other), ale často je zbytečný, pokud kód uspořádáš správně.
2.7 Trivialita
Typ je triviálně kopírovatelný, pokud jeho kopírování je jen kopie bajtů. Knihovna pak použije memcpy, a proto je std::vector<int> tak rychlý.
#include <string>
#include <type_traits>
struct Point { int x, y; };
static_assert(std::is_trivially_copyable_v<Point>);
static_assert(!std::is_trivially_copyable_v<std::string>);
2.8 Typické chyby
return std::move(local);zabrání optimalizaci. Vrať jenlocal.- Chybějící
noexceptna přesunu. - Přesun z
constobjektu. - Po přesunu použití obsahu zdroje.
- Definice destruktoru bez ostatních funkcí.
2.9 Kvíz
2.10 Příklady na procvičení
Máš std::unique_ptr<int> p; a auto q = p;. Proč to nejde přeložit a jak to opravíš?
Řešení
Kopírovací konstruktor je smazaný, protože vlastnictví je jedinečné. Použij auto q = std::move(p);, po němž je p prázdný.
Který konstruktor zavolá T a = f();, když f vrací T hodnotou?
Řešení
Od C++17 žádný navíc: a vznikne přímo z prvalue. Viz kapitola o výkonu.
2.11 Tahák
- Lhodnota existuje dál. Rhodnota se dá vykrást.
- Jméno je vždy lhodnota.
T&jen lhodnoty,const T&vše,T&&jen rhodnoty.- Zdroj = pětice. Hotové typy = nula.
- Přesun:
noexcept, zdroj nech prázdný,std::exchange. std::moveje přetypování.- Moved-from: platný, nespecifikovaný.
- Triviálně kopírovatelný typ =
memcpy.