C++
Przenieś semantykę
Szukaj…
Przenieś semantykę
Przestaw semantykę to sposób przenoszenia jednego obiektu do drugiego w C ++. W tym celu opróżniamy stary obiekt i umieszczamy wszystko, co miał w nowym obiekcie.
W tym celu musimy zrozumieć, czym jest odwołanie do wartości. Odwołanie do wartości ( T&&
gdzie T jest typem obiektu) nie różni się zbytnio od normalnego odwołania ( T&
, teraz nazywane referencjami lvalue). Działają one jednak jako 2 różne typy, dlatego możemy tworzyć konstruktory lub funkcje, które przyjmują jeden lub drugi typ, co będzie konieczne w przypadku semantyki ruchu.
Powodem, dla którego potrzebujemy dwóch różnych typów, jest określenie dwóch różnych zachowań. Konstruktory odniesienia wartości są powiązane z kopiowaniem, natomiast konstruktory odniesienia wartości są związane z przenoszeniem.
Aby przenieść obiekt, użyjemy std::move(obj)
. Ta funkcja zwraca odwołanie do wartości obiektu, dzięki czemu możemy ukraść dane z tego obiektu do nowego. Można to zrobić na kilka sposobów, które omówiono poniżej.
Należy zauważyć, że użycie std::move
tworzy tylko odwołanie do wartości. Innymi słowy, instrukcja std::move(obj)
nie zmienia zawartości obj, podczas gdy auto obj2 = std::move(obj)
(ewentualnie) tak.
Przenieś konstruktora
Powiedzmy, że mamy ten fragment kodu.
class A {
public:
int a;
int b;
A(const A &other) {
this->a = other.a;
this->b = other.b;
}
};
Aby stworzyć konstruktor kopiujący, to znaczy stworzyć funkcję, która kopiuje obiekt i tworzy nowy, normalnie wybieralibyśmy składnię przedstawioną powyżej, mielibyśmy konstruktor dla A, który odwołuje się do innego obiektu typu A, i skopiowalibyśmy obiekt ręcznie wewnątrz metody.
Alternatywnie moglibyśmy napisać A(const A &) = default;
który automatycznie kopiuje wszystkich członków, korzystając z konstruktora kopii.
Aby stworzyć konstruktor ruchów, weźmiemy jednak odwołanie do wartości zamiast odniesienia do wartości, jak tutaj.
class Wallet {
public:
int nrOfDollars;
Wallet() = default; //default ctor
Wallet(Wallet &&other) {
this->nrOfDollars = other.nrOfDollars;
other.nrOfDollars = 0;
}
};
Zauważ, że ustawiliśmy stare wartości na zero
. Domyślny konstruktor ruchu ( Wallet(Wallet&&) = default;
) kopiuje wartość nrOfDollars
, ponieważ jest to POD.
Ponieważ semantyka ruchu została zaprojektowana w celu umożliwienia stanu „kradzieży” z oryginalnej instancji, ważne jest, aby rozważyć, jak powinna wyglądać oryginalna instancja po tej kradzieży. W takim przypadku, gdybyśmy nie zmienili wartości na zero, podwoilibyśmy kwotę dolarów.
Wallet a;
a.nrOfDollars = 1;
Wallet b (std::move(a)); //calling B(B&& other);
std::cout << a.nrOfDollars << std::endl; //0
std::cout << b.nrOfDollars << std::endl; //1
W ten sposób przenieśliśmy skonstruowany obiekt ze starego.
Chociaż powyższy jest prostym przykładem, pokazuje, co ma robić konstruktor ruchu. Staje się bardziej przydatny w bardziej złożonych przypadkach, na przykład w przypadku zarządzania zasobami.
// Manages operations involving a specified type.
// Owns a helper on the heap, and one in its memory (presumably on the stack).
// Both helpers are DefaultConstructible, CopyConstructible, and MoveConstructible.
template<typename T,
template<typename> typename HeapHelper,
template<typename> typename StackHelper>
class OperationsManager {
using MyType = OperationsManager<T, HeapHelper, StackHelper>;
HeapHelper<T>* h_helper;
StackHelper<T> s_helper;
// ...
public:
// Default constructor & Rule of Five.
OperationsManager() : h_helper(new HeapHelper<T>) {}
OperationsManager(const MyType& other)
: h_helper(new HeapHelper<T>(*other.h_helper)), s_helper(other.s_helper) {}
MyType& operator=(MyType copy) {
swap(*this, copy);
return *this;
}
~OperationsManager() {
if (h_helper) { delete h_helper; }
}
// Move constructor (without swap()).
// Takes other's HeapHelper<T>*.
// Takes other's StackHelper<T>, by forcing the use of StackHelper<T>'s move constructor.
// Replaces other's HeapHelper<T>* with nullptr, to keep other from deleting our shiny
// new helper when it's destroyed.
OperationsManager(MyType&& other) noexcept
: h_helper(other.h_helper),
s_helper(std::move(other.s_helper)) {
other.h_helper = nullptr;
}
// Move constructor (with swap()).
// Places our members in the condition we want other's to be in, then switches members
// with other.
// OperationsManager(MyType&& other) noexcept : h_helper(nullptr) {
// swap(*this, other);
// }
// Copy/move helper.
friend void swap(MyType& left, MyType& right) noexcept {
std::swap(left.h_helper, right.h_helper);
std::swap(left.s_helper, right.s_helper);
}
};
Przenieś zadanie
Podobnie do tego, w jaki sposób możemy przypisać wartość do obiektu za pomocą odwołania do wartości, kopiując go, możemy również przenosić wartości z obiektu na inny bez budowania nowego. Nazywamy to przeniesieniem. Przenosimy wartości z jednego obiektu do innego istniejącego obiektu.
W tym celu będziemy musieli przeciążać operator =
, nie tak, aby pobierał odwołanie do wartości, jak w przypadku przypisania kopii, ale aby pobierał odwołanie do wartości.
class A {
int a;
A& operator= (A&& other) {
this->a = other.a;
other.a = 0;
return *this;
}
};
Jest to typowa składnia definiująca przypisanie ruchu. Przeciążamy operator =
, abyśmy mogli podać mu odwołanie do wartości i przypisać go do innego obiektu.
A a;
a.a = 1;
A b;
b = std::move(a); //calling A& operator= (A&& other)
std::cout << a.a << std::endl; //0
std::cout << b.a << std::endl; //1
W ten sposób możemy przenieść przypisanie obiektu do innego.
Używanie std :: move w celu zmniejszenia złożoności z O (n²) do O (n)
C ++ 11 wprowadził podstawowy język i standardową obsługę bibliotek do przenoszenia obiektu. Chodzi o to, że gdy obiekt o jest tymczasowy i chce logiczną kopię, a następnie jego bezpieczne zaledwie pilfer O „s zasobów, takich jak dynamicznie przydzielonego buforu, pozostawiając o logicznie pusty, ale nadal zniszczalne i copyable.
Obsługa języka podstawowego jest głównie
konstruktor typów odniesień do wartości
&&
, np.std::string&&
to odwołanie do wartościstd::string
, wskazujące, że to odniesienie do obiektu jest tymczasowe, którego zasoby można po prostu sfałszować (tj. przenieść)specjalne wsparcie dla konstruktora przenoszenia
T( T&& )
, który ma efektywnie przenosić zasoby z określonego innego obiektu, zamiast kopiować te zasoby, orazspecjalne wsparcie dla operatora przypisania ruchu
auto operator=(T&&) -> T&
, który również powinien przenieść się ze źródła.
Standardową obsługą bibliotek jest głównie szablon funkcji std::move
z nagłówka <utility>
. Ta funkcja generuje odwołanie do wartości określonego obiektu, wskazując, że można go przenieść, tak jakby był tymczasowy.
W przypadku kontenera faktyczne kopiowanie ma zazwyczaj złożoność O ( n ), gdzie n jest liczbą elementów w kontenerze, podczas gdy przenoszenie to O (1), stały czas. W przypadku algorytmu, który logicznie kopiuje ten pojemnik n razy, może to zmniejszyć złożoność z zwykle niepraktycznego O ( n ²) do po prostu liniowego O ( n ).
W swoim artykule „Kontenery, które nigdy się nie zmieniają” w czasopiśmie dr Dobbs we wrześniu 19 2013 roku Andrew Koenig przedstawił interesujący przykład nieefektywności algorytmicznej przy użyciu stylu programowania, w którym zmienne są niezmienne po inicjalizacji. W tym stylu pętle są zazwyczaj wyrażane za pomocą rekurencji. W przypadku niektórych algorytmów, takich jak generowanie sekwencji Collatz, rekurencja wymaga logicznego skopiowania kontenera:
// Based on an example by Andrew Koenig in his Dr. Dobbs Journal article
// “Containers That Never Change” September 19, 2013, available at
// <url: http://www.drdobbs.com/cpp/containters-that-never-change/240161543>
// Includes here, e.g. <vector>
namespace my {
template< class Item >
using Vector_ = /* E.g. std::vector<Item> */;
auto concat( Vector_<int> const& v, int const x )
-> Vector_<int>
{
auto result{ v };
result.push_back( x );
return result;
}
auto collatz_aux( int const n, Vector_<int> const& result )
-> Vector_<int>
{
if( n == 1 )
{
return result;
}
auto const new_result = concat( result, n );
if( n % 2 == 0 )
{
return collatz_aux( n/2, new_result );
}
else
{
return collatz_aux( 3*n + 1, new_result );
}
}
auto collatz( int const n )
-> Vector_<int>
{
assert( n != 0 );
return collatz_aux( n, Vector_<int>() );
}
} // namespace my
#include <iostream>
using namespace std;
auto main() -> int
{
for( int const x : my::collatz( 42 ) )
{
cout << x << ' ';
}
cout << '\n';
}
Wynik:
42 21 64 32 16 8 4 2
Liczba operacji kopiowania elementów z powodu kopiowania wektorów jest w przybliżeniu O ( n² ), ponieważ jest to suma 1 + 2 + 3 + ... n .
W konkretnych liczbach, w kompilatorach g ++ i Visual C ++ powyższe wywołanie collatz(42)
skutkowało sekwencją Collatz 8 pozycji i 36 operacji kopiowania pozycji (8 * collatz(42)
= 28, plus niektóre) w wywołaniach konstruktora kopii wektorowej.
Wszystkie te operacje kopiowania elementów można usunąć, po prostu przesuwając wektory, których wartości nie są już potrzebne. Aby to zrobić, konieczne jest usunięcie const
i odwołania do argumentów typu wektorowego, przekazując wektory według wartości . Zwroty funkcji są już automatycznie optymalizowane. W przypadku wywołań, w których wektory są przekazywane i nie są ponownie używane w funkcji, po prostu zastosuj std::move
aby przenieść te bufory, zamiast je kopiować:
using std::move;
auto concat( Vector_<int> v, int const x )
-> Vector_<int>
{
v.push_back( x );
// warning: moving a local object in a return statement prevents copy elision [-Wpessimizing-move]
// See https://stackoverflow.com/documentation/c%2b%2b/2489/copy-elision
// return move( v );
return v;
}
auto collatz_aux( int const n, Vector_<int> result )
-> Vector_<int>
{
if( n == 1 )
{
return result;
}
auto new_result = concat( move( result ), n );
struct result; // Make absolutely sure no use of `result` after this.
if( n % 2 == 0 )
{
return collatz_aux( n/2, move( new_result ) );
}
else
{
return collatz_aux( 3*n + 1, move( new_result ) );
}
}
auto collatz( int const n )
-> Vector_<int>
{
assert( n != 0 );
return collatz_aux( n, Vector_<int>() );
}
Tutaj, w kompilatorach g ++ i Visual C ++, liczba operacji kopiowania elementów spowodowanych wywołaniami konstruktora kopii wektorowej wynosiła dokładnie 0.
Algorytm z konieczności ma wciąż długość O ( n ) w długości wytworzonej sekwencji Collatz, ale jest to dość radykalna poprawa: O ( n ²) → O ( n ).
Przy pewnym wsparciu językowym można być może użyć ruchu i nadal wyrażać i egzekwować niezmienność zmiennej między jej inicjalizacją a końcowym ruchem , po czym każde użycie tej zmiennej powinno być błędem. Niestety, od C ++ 14 C ++ tego nie obsługuje. W przypadku kodu bez pętli nie można używać po przeniesieniu poprzez ponowne zadeklarowanie odpowiedniej nazwy jako niekompletnej struct
, jak w przypadku struct result;
powyżej, ale jest to brzydkie i prawdopodobnie nie zostanie zrozumiane przez innych programistów; także diagnostyka może być dość myląca.
Podsumowując, obsługa języka i biblioteki C ++ dla przenoszenia pozwala na drastyczne ulepszenia złożoności algorytmu, ale z powodu niekompletności wsparcia, kosztem rezygnacji z gwarancji poprawności kodu i przejrzystości kodu, które może zapewnić const
.
Dla kompletności klasa wektorów instrumentowanych używana do pomiaru liczby operacji kopiowania elementów z powodu wywołań konstruktora kopii:
template< class Item >
class Copy_tracking_vector
{
private:
static auto n_copy_ops()
-> int&
{
static int value;
return value;
}
vector<Item> items_;
public:
static auto n() -> int { return n_copy_ops(); }
void push_back( Item const& o ) { items_.push_back( o ); }
auto begin() const { return items_.begin(); }
auto end() const { return items_.end(); }
Copy_tracking_vector(){}
Copy_tracking_vector( Copy_tracking_vector const& other )
: items_( other.items_ )
{ n_copy_ops() += items_.size(); }
Copy_tracking_vector( Copy_tracking_vector&& other )
: items_( move( other.items_ ) )
{}
};
Używanie semantyki przenoszenia na kontenerach
Możesz przenieść kontener zamiast go skopiować:
void print(const std::vector<int>& vec) {
for (auto&& val : vec) {
std::cout << val << ", ";
}
std::cout << std::endl;
}
int main() {
// initialize vec1 with 1, 2, 3, 4 and vec2 as an empty vector
std::vector<int> vec1{1, 2, 3, 4};
std::vector<int> vec2;
// The following line will print 1, 2, 3, 4
print(vec1);
// The following line will print a new line
print(vec2);
// The vector vec2 is assigned with move assingment.
// This will "steal" the value of vec1 without copying it.
vec2 = std::move(vec1);
// Here the vec1 object is in an indeterminate state, but still valid.
// The object vec1 is not destroyed,
// but there's is no guarantees about what it contains.
// The following line will print 1, 2, 3, 4
print(vec2);
}
Ponownie użyj przeniesionego obiektu
Możesz ponownie użyć przeniesionego obiektu:
void consumingFunction(std::vector<int> vec) {
// Some operations
}
int main() {
// initialize vec with 1, 2, 3, 4
std::vector<int> vec{1, 2, 3, 4};
// Send the vector by move
consumingFunction(std::move(vec));
// Here the vec object is in an indeterminate state.
// Since the object is not destroyed, we can assign it a new content.
// We will, in this case, assign an empty value to the vector,
// making it effectively empty
vec = {};
// Since the vector as gained a determinate value, we can use it normally.
vec.push_back(42);
// Send the vector by move again.
consumingFunction(std::move(vec));
}