Semantyka przenoszenia (Move semantics)¶
Motywacja¶
Optymalizacja wydajności:
możliwość rozpoznania kiedy mamy do czynienia z obiektem tymczasowym (temporary object)
możliwość wskazania, że obiekt nie będzie dalej używany - jego czas życia wygasa (expiring object)
Możliwość implementacji obiektów, które są nie powinny być kopiowane, ale umożliwiają transfer prawa własności do zasobu:
auto_ptr<T>
w C++98 symulował semantykę przenoszenia za pomocą konstruktora kopiującego i operatora przypisaniaobiekty kontrolujące zasoby systemowe, które nie mogą być łatwo kopiowane - wątki, pliki, strumienie, itp.
void create_and_insert(vector<string>& coll)
{
string str = "text";
coll.push_back(str); // insert a copy of str
// str is used later
coll.push_back(str + str); // insert a copy of temporary value
// unnecessary copy in C++98
coll.push_back("text"); // insert a copy of temporary value
// unnecessary copy in C++98
coll.push_back(str); // insert a copy of str
// unnecessary copy in C++98
// str is no longer used
}
lvalues i rvalues¶
Aby umożliwić implementację semantyki przenoszenia C++11 wprowadza podział obiektów na:
lvalue
obiekt posiada nazwę
można pobrać adres obiektu
rvalue
nie można pobrać adresu
zwykle nienazwany obiekt tymczasowy (np. obiekt zwrócony z funkcji)
z obiektów rvalue możemy transferować stan pozostawiając je w poprawnym, ale nieokreślonym stanie
Przykłady:
double dx;
double* ptr; // dx and ptr are lvalues
std::size_t foo(std::string str); // foo and str are lvalues
// f's return is rvalue
foo("Hello"); // temp string created for call is rvalue
std::vector<int> vec; // vec is lvalue
vi[5] = 0; // vi[5] is lvalue
Operacja przenoszenia, która wiąże się ze zmianą stanu jest niebezpieczna dla obiektów lvalue ponieważ obiekt może zostać użyty po wykonaniu takiej operacji.
Operacje przenoszenia są bezpieczne dla obiektów rvalue.
Referencje rvalue - rvalue references¶
C++11 wprowadza referencje do rvalue - rvalue references, które zachowują się podobnie jak klasyczne referencje z C++98 (zwane w C++11 lvalue references).
składnia: T&&
muszą zostać zainicjowane i nie mogą zmienić odniesienia
służą do identyfikacji operacji, które implementują przenoszenie
Wprowadzenie referencji do rvalue rozszerza reguły wiązania referencji:
Tak jak w C++98:
lvalues mogą być wiązane do lvalue references
rvalues mogą być wiązanie do const lvalue references
W C++11:
rvalues mogą być wiązane do rvalue references
lvalues nie mogą być wiązane do rvalue references
Ważne
Stałe obiekty lvalue lub rvalue mogą być wiązane tylko z referencjami do const (const T&&
są poprawne składniowo, ale nie mają sensu).
Implementacja semantyki przenoszenia¶
Używając rvalue references możemy zaimplementować semantykę przenoszenia.
template <typename T>
class vector
{
public:
void push_back(const T& item); // inserts a copy of item
void push_back(T&& item); // moves item into container
};
// ...
void create_and_insert(vector<string>& coll)
{
string str = "text";
coll.push_back(str); // insert a copy of str
// str is used later
coll.push_back(str + str); // rvalue binds to push_back(string&&)
// temp is moved into container
coll.push_back("text"); // rvalue binds to push_back(string&&)
// tries to move temporary objecy into container
coll.push_back(std::move(str)); // tries to move str object into container
// str is no longer used
}
Innym przykładem mało wydajnej implementacji z wykorzystaniem kopiowania jest implementacja swap
w C++98:
template <typename T>
void swap(T& a, T& b)
{
T temp = a; // copy a to temp
a = b; // copy b to a
b = temp; // copy temp to b
} // destroy temp
Funkcja swap()
może zostać wydajniej zaimplementowana w C++11 z wykorzystaniem semantyki przenoszenia - zamiast kopiować przenosimy
wewnętrzny stan obiektów (np. wskaźniki do zasobów):
#include <utility>
template <typename T>
void swap(T& a, T& b)
{
T temp {std::move(a)}; // tries to move a to temp
a = std::move(b); // tries to move b to a
b = std::move(temp); // tries to move temp to b
} // destroy temp
Semantyka przenoszenia w klasach¶
Aby zaimplementować semantykę przenoszenia dla klasy należy zapewnić jej:
konstruktor przenoszący - przyjmujący jako argument rvalue reference
przenoszący operator przypisania - przyjmujący jako argument rvalue reference
Ważne
Konstruktor przenoszący i przenoszący operator przypisania są nowymi specjalnymi funkcjami składowymi klas w C++11.
Funkcje specjalne klas w C++11¶
W C++11 istnieje sześć specjalnych funkcji składowych klasy:
konstruktor domyślny -
X();
destruktor -
~X();
konstruktor kopiujący -
X(const X&);
kopiujacy operator przypisania -
X& operator=(const X&);
konstruktor przenoszący -
X(X&&);
przenoszący operator przypisania -
X& operator=(X&&);
Specjalne funkcje mogą być:
nie zadeklarowane - not declared
niejawnie zadeklarowane - implicitly declared
zadeklarowane przez użytkownika - user declared
Specjalne funkcje zdefiniowane jako = default
są traktowane jako user declared.
Klasy domyślnie implementują semantykę przenoszenia.
Domyślny konstruktor przenoszący przenosi każdą składową klasy.
Domyślny przenoszący operator przypisania deleguje semantykę przenoszenia do każdej składowej klasy
Konceptualny kod domyślnego konstruktora przenoszącego i przenoszącego operatora przypisania:
class X : public Base
{
Member m_;
X(X&& x) : Base(static_cast<Base&&>(x)), m_(static_cast<Member&&>(x.m_))
{}
X& operator=(X&& )
{
Base::operator=(static_cast<Base&&>(x));
m_ = static_cast<X&&>(x.m_);
return *this;
}
};
Przenoszące funkcje specjalne implementowane przez użytkownika:
class X : public Base
{
Member m_;
X(X&& x) : Base(std::move(x)), m_(std::move(x.m_))
{
x.set_to_resourceless_state();
}
X& operator=(X&& )
{
Base::operator=(std::move(x));
m_ = std::move(x.m_);
x.set_to_resourceless_state();
return *this;
}
};
Jeśli klasa nie zapewnia prawidłowej semantyki przenoszenia - w rezultacie wykonania operacji move()
odbywa się kopiowanie.
Reguła =default
¶
Jeżeli jedna z poniższych funkcji specjalnych klasy jest user declared
konstruktor kopiujący
kopiujący operator przypisania
destruktor
jedna z przenoszących funkcji specjalnych
specjalne funkcje przenoszące nie są generowane przez kompilator i operacja przenoszenia jest implementowana poprzez kopiowanie elementu (fallback to copy).
Klasa:
class Gadget // default copy and move semantics enabled
{
};
nie jest równoważna klasie:
class Gadget // default copy and move semantics disabled
{
~Gadget() = default;
};
Aby umożliwić przenoszenie należy zdefiniować (najlepiej) wszystkie funkcje specjalne.
Ważne
If one =delete
, define all special member functions!
Reguła „Rule of Five”¶
Jeśli w klasie jest konieczna implementacja jednej z poniższych specjalnych funkcji składowych:
konstruktora kopiującego
konstruktora przenoszącego
kopiującego operatora przypisania
przenoszącego operatora przypisania
destruktora
najprawdopodobniej należy zaimplementować wszystkie.
Ta regułą stosuje się również do funkcji specjalnych zdefiniowanych jako default
.