Čtení: 40 minHodnotí se: úloha 2, zkouška

2 Hodnoty, reference a přesun

Po přečtení umíš
  • určit hodnotovou kategorii výrazu a co na něj jde navázat
  • vysvětlit, co dělá std::move a 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

Definice 2.2 (Lhodnota a rhodnota)

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í

Definice 2.3 (Pravidlo pěti a nuly)

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í:

2.5 std::move nic nepřesouvá

Poznámka 2.4 (Je to přetypování)

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

2.9 Kvíz

Kvíz

1. Je x v void f(int&& x) { g(x); } lhodnota, nebo rhodnota?
2. Co dělá std::move(x) za běhu?
3. Na co se naváže const T&? Může být správně více možností, ale také žádná.
4. Proč má být přesunový konstruktor noexcept?
5. Co je std::vector<std::string> po auto b = std::move(a);?
6. Jak copy-and-swap zajistí silnou záruku?
Ukázat řešeníKopie vznikne v parametru dřív, než se cíl změní. Pokud selže, cíl je netknutý. Výměna je noexcept.

2.10 Příklady na procvičení

Cvičení 2.5

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ý.

Cvičení 2.6

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

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::move je přetypování.
  • Moved-from: platný, nespecifikovaný.
  • Triviálně kopírovatelný typ = memcpy.

Zdroje

  • ni_epc_05: výrazy strany 3–11, reference 12–25, obsah objektů 26–35, přetěžování 36–42, inicializace a přiřazení 43–45
  • ni_epc_06: kopírování strany 6–15, přesun 17–35, kopírování a přesun 37–42, trivialita 43–48