22.3 -- Przenieś konstruktory i przenieś przypisanie

W lekcji 22.1 -- Wprowadzenie do inteligentnych wskaźników i semantyki ruchów, przyjrzeliśmy się std::auto_ptr, omówiliśmy potrzebę semantyki przenoszenia i przyjrzeliśmy się niektórym wadom, które występują, gdy funkcje przeznaczone do semantyki kopiowania (konstruktory kopiowania i operatory przypisania kopiowania) są przedefiniowane w celu implementacji semantyki przenoszenia.

W tej lekcji przyjrzymy się bliżej, jak C++11 rozwiązuje te problemy za pomocą konstruktorów przenoszenia i przenoszenia przypisanie.

Podsumowanie konstruktorów kopiujących i przypisanie kopii

Najpierw poświęćmy chwilę na podsumowanie semantyki kopiowania.

Konstruktory kopiujące służą do inicjowania klasy poprzez utworzenie kopii obiektu tej samej klasy. Przypisanie kopiujące służy do kopiowania jednego obiektu klasy do innego istniejącego obiektu klasy. Domyślnie C++ udostępnia konstruktor kopiujący i operator przypisania kopiowania, jeśli nie został on wyraźnie podany. Te funkcje dostarczane przez kompilator wykonują płytkie kopie, co może powodować problemy dla klas, które alokują pamięć dynamiczną. Zatem klasy zajmujące się pamięcią dynamiczną powinny zastąpić te funkcje, aby móc wykonywać głębokie kopie.

Wracając do naszego przykładu klasy inteligentnego wskaźnika Auto_ptr z pierwszej lekcji w tym rozdziale, spójrzmy na wersję, która implementuje konstruktor kopiujący i operator przypisania kopiowania, które wykonują głębokie kopie, oraz przykładowy program, który je wykonuje:

#include <iostream>

template<typename T>
class Auto_ptr3
{
	T* m_ptr {};
public:
	Auto_ptr3(T* ptr = nullptr)
		: m_ptr { ptr }
	{
	}

	~Auto_ptr3()
	{
		delete m_ptr;
	}

	// Copy constructor
	// Do deep copy of a.m_ptr to m_ptr
	Auto_ptr3(const Auto_ptr3& a)
	{
		m_ptr = new T;
		*m_ptr = *a.m_ptr;
	}

	// Copy assignment
	// Do deep copy of a.m_ptr to m_ptr
	Auto_ptr3& operator=(const Auto_ptr3& a)
	{
		// Self-assignment detection
		if (&a == this)
			return *this;

		// Release any resource we're holding
		delete m_ptr;

		// Copy the resource
		m_ptr = new T;
		*m_ptr = *a.m_ptr;

		return *this;
	}

	T& operator*() const { return *m_ptr; }
	T* operator->() const { return m_ptr; }
	bool isNull() const { return m_ptr == nullptr; }
};

class Resource
{
public:
	Resource() { std::cout << "Resource acquired\n"; }
	~Resource() { std::cout << "Resource destroyed\n"; }
};

Auto_ptr3<Resource> generateResource()
{
	Auto_ptr3<Resource> res{new Resource};
	return res; // this return value will invoke the copy constructor
}

int main()
{
	Auto_ptr3<Resource> mainres;
	mainres = generateResource(); // this assignment will invoke the copy assignment

	return 0;
}

W tym programie używamy funkcji o nazwie generateResource() do tworzenia zasobu zawartego w inteligentnym wskaźniku, który jest następnie przekazywany z powrotem do funkcji main(). Funkcja main() następnie przypisuje to do istniejącego obiektu Auto_ptr3.

Po uruchomieniu tego programu wypisuje:

Resource acquired
Resource acquired
Resource destroyed
Resource acquired
Resource destroyed
Resource destroyed

(Uwaga: możesz otrzymać tylko 4 wyniki, jeśli kompilator pominie wartość zwracaną przez funkcję generateResource())

W przypadku tak prostego programu dzieje się dużo tworzenia i niszczenia zasobów! Co tu się dzieje?

Przyjrzyjmy się bliżej. Program ten składa się z 6 kluczowych kroków (po jednym dla każdego drukowanego komunikatu):

  1. W środku generateResource() tworzona jest zmienna lokalna res, która jest inicjalizowana dynamicznie przydzielanym zasobem, co powoduje pierwszy „Zdobyty zasób”.
  2. Res jest zwracany z powrotem do main() według wartości. Zwracamy tutaj wartość, ponieważ res jest zmienną lokalną — nie można jej zwrócić przez adres ani referencję, ponieważ res zostanie zniszczony po zakończeniu generateResource(). Zatem res to kopia zbudowana w obiekcie tymczasowym. Ponieważ nasz konstruktor kopiujący wykonuje głęboką kopię, przydzielany jest tutaj nowy zasób, co powoduje drugi „Zasób zdobyty”.
  3. Res wykracza poza zakres, niszcząc oryginalnie utworzony Zasób, co powoduje pierwszy „Zasób zniszczony”.
  4. Obiekt tymczasowy jest przypisywany do mainres poprzez przypisanie kopii. Ponieważ nasze przypisanie kopiujące wykonuje również głęboką kopię, przydzielany jest nowy zasób, powodując kolejny „Zasób zdobyty”.
  5. Wyrażenie przypisania kończy się, a obiekt tymczasowy wychodzi poza zakres wyrażenia i zostaje zniszczony, powodując „Zasób zniszczony”.
  6. Pod koniec funkcji main() mainres wychodzi poza zakres i wyświetla się nasz końcowy „Zasób zniszczony”.

A więc w skrócie, ponieważ raz wywołujemy konstruktor kopiujący w celu skopiowania skonstruuj res do tymczasowego i raz skopiuj przypisanie, aby skopiować tymczasowe do mainres, kończy się to alokacją i zniszczeniem w sumie 3 oddzielnych obiektów.

Nieefektywne, ale przynajmniej nie ulega awarii!

Jednak dzięki semantyce przenoszenia możemy działać lepiej.

Przenieś konstruktory i przenieś przypisanie

C++11 definiuje dwie nowe funkcje obsługujące semantykę przenoszenia: konstruktor przenoszenia i operator przypisania przenoszenia. Podczas gdy celem konstruktora kopiującego i przypisania kopiowania jest utworzenie kopii jednego obiektu na drugim, celem konstruktora przenoszenia i przypisania przenoszenia jest przeniesienie własności zasobów z jednego obiektu na inny (co jest zwykle znacznie tańsze niż wykonanie kopii).

Definiowanie konstruktora przenoszenia i przydziału przenoszenia odbywa się analogicznie do ich odpowiedników kopiowania. Jednakże, podczas gdy warianty kopiowania tych funkcji przyjmują parametr referencyjny o stałej wartości l (który będzie wiązał się z niemal wszystkim), warianty przenoszenia tych funkcji wykorzystują parametry referencyjne o wartościach innych niż stałe (które wiążą się tylko z wartościami).

Oto ta sama klasa Auto_ptr3 co powyżej, z dodanym konstruktorem przenoszenia i operatorem przypisania przenoszenia. Dla celów porównawczych pozostawiliśmy konstruktor kopiujący do głębokiego kopiowania i operator przypisania kopiowania.

#include <iostream>

template<typename T>
class Auto_ptr4
{
	T* m_ptr {};
public:
	Auto_ptr4(T* ptr = nullptr)
		: m_ptr { ptr }
	{
	}

	~Auto_ptr4()
	{
		delete m_ptr;
	}

	// Copy constructor
	// Do deep copy of a.m_ptr to m_ptr
	Auto_ptr4(const Auto_ptr4& a)
	{
		m_ptr = new T;
		*m_ptr = *a.m_ptr;
	}

	// Move constructor
	// Transfer ownership of a.m_ptr to m_ptr
	Auto_ptr4(Auto_ptr4&& a) noexcept
		: m_ptr { a.m_ptr }
	{
		a.m_ptr = nullptr; // we'll talk more about this line below
	}

	// Copy assignment
	// Do deep copy of a.m_ptr to m_ptr
	Auto_ptr4& operator=(const Auto_ptr4& a)
	{
		// Self-assignment detection
		if (&a == this)
			return *this;

		// Release any resource we're holding
		delete m_ptr;

		// Copy the resource
		m_ptr = new T;
		*m_ptr = *a.m_ptr;

		return *this;
	}

	// Move assignment
	// Transfer ownership of a.m_ptr to m_ptr
	Auto_ptr4& operator=(Auto_ptr4&& a) noexcept
	{
		// Self-assignment detection
		if (&a == this)
			return *this;

		// Release any resource we're holding
		delete m_ptr;

		// Transfer ownership of a.m_ptr to m_ptr
		m_ptr = a.m_ptr;
		a.m_ptr = nullptr; // we'll talk more about this line below

		return *this;
	}

	T& operator*() const { return *m_ptr; }
	T* operator->() const { return m_ptr; }
	bool isNull() const { return m_ptr == nullptr; }
};

class Resource
{
public:
	Resource() { std::cout << "Resource acquired\n"; }
	~Resource() { std::cout << "Resource destroyed\n"; }
};

Auto_ptr4<Resource> generateResource()
{
	Auto_ptr4<Resource> res{new Resource};
	return res; // this return value will invoke the move constructor
}

int main()
{
	Auto_ptr4<Resource> mainres;
	mainres = generateResource(); // this assignment will invoke the move assignment

	return 0;
}

Konstruktor przenoszenia i operator przypisania przenoszenia są proste. Zamiast głęboko kopiować obiekt źródłowy (a) do obiektu docelowego (obiekt ukryty), po prostu przenosimy (kradniemy) zasoby obiektu źródłowego. Wiąże się to z płytkim skopiowaniem wskaźnika źródła do ukrytego obiektu, a następnie ustawieniem wskaźnika źródła na wartość null.

Po uruchomieniu ten program wypisuje:

Resource acquired
Resource destroyed

Tak jest znacznie lepiej!

Przebieg programu jest dokładnie taki sam jak poprzednio. Jednak zamiast wywoływać konstruktor kopiujący i operatory przypisania kopiowania, program ten wywołuje konstruktor przenoszenia i operatory przypisania przenoszenia. Patrząc nieco głębiej:

  1. W środku generateResource() tworzona jest zmienna lokalna res, która jest inicjalizowana dynamicznie przydzielanym zasobem, co powoduje pierwszy „Zdobyty zasób”.
  2. Res jest zwracany z powrotem do main() według wartości. Res to przeniesienie skonstruowane do obiektu tymczasowego, przeniesienie dynamicznie utworzonego obiektu przechowywanego w res do obiektu tymczasowego. Poniżej porozmawiamy o tym, dlaczego tak się dzieje.
  3. Res wykracza poza zakres. Ponieważ res nie zarządza już wskaźnikiem (został przeniesiony do tymczasowego), nie dzieje się tu nic ciekawego.
  4. Obiekt tymczasowy jest przenoszony do mainres. Spowoduje to przeniesienie dynamicznie utworzonego obiektu przechowywanego w pliku tymczasowym do pliku mainres.
  5. Wyrażenie przypisania kończy się, a obiekt tymczasowy wychodzi poza zakres wyrażenia i ulega zniszczeniu. Ponieważ jednak element tymczasowy nie zarządza już wskaźnikiem (został przeniesiony do mainres), również tutaj nie dzieje się nic ciekawego.
  6. Pod koniec funkcji main() mainres wychodzi poza zakres i wyświetla się nasz końcowy „Zasób zniszczony”.

Zamiast więc kopiować nasz zasób dwa razy (raz dla konstruktora kopiującego i raz dla przypisania kopii), przesyłamy go dwukrotnie. Jest to bardziej wydajne, ponieważ zasób jest budowany i niszczony tylko raz, a nie trzy razy.

Powiązana treść

Konstruktory przenoszenia i przypisanie przenoszenia powinny być oznaczone jako noexcept. Mówi to kompilatorowi, że te funkcje nie będą zgłaszać wyjątków.

Wprowadzamy noexcept w lekcji 27,9 — specyfikacje wyjątków i noexcept i omawiamy, dlaczego konstruktory przenoszenia i przypisanie przenoszenia są oznaczone jako noexcept w lekcji 27.10 — std::move_if_noexcept.

Kiedy wywoływany jest konstruktor przenoszenia i przypisanie przenoszenia?

Konstruktor przenoszenia i przypisanie przenoszenia są wywoływane, gdy te funkcje są zdefiniowane, a argumentem konstrukcji lub przypisania jest wartość. Najczęściej ta wartość będzie wartością dosłowną lub tymczasową.

Konstruktor kopiujący i przypisanie kopii są używane w inny sposób (kiedy argument jest wartością lub gdy argument jest wartością, a konstruktor przenoszenia lub funkcje przypisania przenoszenia nie są zdefiniowane).

Niejawny konstruktor przenoszenia i operator przypisania przenoszenia

Kompilator utworzy niejawny konstruktor przenoszenia i operator przypisania przenoszenia, jeśli spełnione są wszystkie poniższe warunki prawda:

  • Nie ma zadeklarowanych przez użytkownika konstruktorów kopiujących ani operatorów przypisania kopiowania.
  • Nie ma zadeklarowanych przez użytkownika konstruktorów przenoszenia ani operatorów przypisania przenoszenia.
  • Nie ma destruktora zadeklarowanego przez użytkownika.

Te funkcje wykonują przenoszenie członowe, które zachowuje się w następujący sposób:

  • Jeśli element ma konstruktor przenoszenia lub przypisanie ruchu (w zależności od przypadku), zostanie to wykonane zostanie wywołany.
  • W przeciwnym razie element zostanie skopiowany.

W szczególności oznacza to, że wskaźniki zostaną skopiowane, a nie przeniesione!

Ostrzeżenie

Niejawny konstruktor przenoszenia i przypisanie przenoszenia skopiują wskaźniki, a nie je przeniosą. Jeśli chcesz przenieść element wskaźnikowy, musisz samodzielnie zdefiniować konstruktor przenoszenia i przypisanie przeniesienia.

Kluczowe informacje dotyczące semantyki przenoszenia

Masz teraz wystarczający kontekst, aby zrozumieć kluczowe informacje stojące za semantyką przenoszenia.

Jeśli konstruujemy obiekt lub wykonujemy zadanie, w którym argumentem jest wartość l, jedyne, co możemy rozsądnie zrobić, to skopiować wartość l. Nie możemy założyć, że zmiana wartości l jest bezpieczna, ponieważ może ona zostać ponownie użyta w dalszej części programu. Jeśli mamy wyrażenie „a = b” (gdzie b jest lwartością), nie moglibyśmy rozsądnie oczekiwać, że b zostanie w jakikolwiek sposób zmienione.

Jeśli jednak skonstruujemy obiekt lub wykonamy zadanie, w którym argumentem jest wartość r, to wiemy, że wartość r jest tylko pewnego rodzaju obiektem tymczasowym. Zamiast go kopiować (co może być kosztowne), możemy po prostu przenieść jego zasoby (co jest tanie) na obiekt, który konstruujemy lub przydzielamy. Jest to bezpieczne, ponieważ element tymczasowy i tak zostanie zniszczony na końcu wyrażenia, więc mamy pewność, że nigdy więcej nie zostanie użyty!

C++ 11, poprzez odniesienia do wartości r, daje nam możliwość zapewnienia różnych zachowań, gdy argumentem jest wartość r w porównaniu z wartością l, umożliwiając nam podejmowanie mądrzejszych i skuteczniejszych decyzji dotyczących zachowania naszych obiektów.

Kluczowa informacja

Semantyka ruchu jest optymalizacją okazja.

Funkcje przenoszenia powinny zawsze pozostawiać oba obiekty w prawidłowym stanie

W powyższych przykładach zarówno konstruktor przenoszenia, jak i funkcje przypisania przenoszenia ustawiają a.m_ptr na nullptr. Może się to wydawać niepotrzebne -- w końcu, jeśli a jest tymczasową wartością r, po co zawracać sobie głowę „czyszczeniem”, jeśli parametr a i tak ma zostać zniszczony?

Odpowiedź jest prosta: Kiedy a wyjdzie poza zakres, zostanie wywołany destruktor dla a , a a.m_ptr zostanie usunięty. Jeśli w tym momencie a.m_ptr wciąż wskazuje na ten sam obiekt, co m_ptr, wówczas m_ptr zostanie pozostawiony jako wiszący wskaźnik. Kiedy obiekt zawierający m_ptr zostanie w końcu użyty (lub zniszczony), otrzymamy niezdefiniowane zachowanie.

Wdrażając semantykę przenoszenia, ważne jest, aby upewnić się, że przenoszony obiekt pozostaje w prawidłowym stanie, tak aby uległ prawidłowemu zniszczeniu (bez tworzenia niezdefiniowanego zachowania).

Automatyczne l-wartości zwracane przez wartość mogą być przenoszone zamiast kopiowane

W generateResource() z powyższego przykładu Auto_ptr4, gdy zmienna res jest zwracana przez wartość, jest ona przenoszona zamiast kopiowana, nawet jeśli res jest wartością l. Specyfikacja C++ ma specjalną regułę, która mówi, że automatyczne obiekty zwracane przez funkcję według wartości można przenosić, nawet jeśli są to l-wartości. Ma to sens, ponieważ res i tak miał zostać zniszczony na końcu funkcji! Równie dobrze moglibyśmy ukraść jego zasoby, zamiast tworzyć kosztowną i niepotrzebną kopię.

Chociaż kompilator może przenosić zwracane wartości o wartości l, w niektórych przypadkach może zrobić to jeszcze lepiej, po prostu całkowicie pomijając kopię (co pozwala uniknąć konieczności tworzenia kopii lub wykonywania jakichkolwiek ruchów). W takim przypadku nie zostanie wywołany ani konstruktor kopiujący, ani konstruktor przenoszący.

Wyłączenie kopiowania

W powyższej klasie Auto_ptr4 pozostawiliśmy konstruktor kopiujący i operator przypisania dla celów porównawczych. Jednak w klasach umożliwiających przenoszenie czasami pożądane jest usunięcie konstruktora kopiującego i funkcji przypisania kopii, aby mieć pewność, że kopie nie zostaną utworzone. W przypadku naszej klasy Auto_ptr nie chcemy kopiować naszego szablonowego obiektu T - zarówno dlatego, że jest drogi, jak i każda klasa T może nawet nie obsługiwać kopiowania!

Oto wersja Auto_ptr, która obsługuje semantykę przenoszenia, ale nie semantykę kopiowania:

#include <iostream>

template<typename T>
class Auto_ptr5
{
	T* m_ptr {};
public:
	Auto_ptr5(T* ptr = nullptr)
		: m_ptr { ptr }
	{
	}

	~Auto_ptr5()
	{
		delete m_ptr;
	}

	// Copy constructor -- no copying allowed!
	Auto_ptr5(const Auto_ptr5& a) = delete;

	// Move constructor
	// Transfer ownership of a.m_ptr to m_ptr
	Auto_ptr5(Auto_ptr5&& a) noexcept
		: m_ptr { a.m_ptr }
	{
		a.m_ptr = nullptr;
	}

	// Copy assignment -- no copying allowed!
	Auto_ptr5& operator=(const Auto_ptr5& a) = delete;

	// Move assignment
	// Transfer ownership of a.m_ptr to m_ptr
	Auto_ptr5& operator=(Auto_ptr5&& a) noexcept
	{
		// Self-assignment detection
		if (&a == this)
			return *this;

		// Release any resource we're holding
		delete m_ptr;

		// Transfer ownership of a.m_ptr to m_ptr
		m_ptr = a.m_ptr;
		a.m_ptr = nullptr;

		return *this;
	}

	T& operator*() const { return *m_ptr; }
	T* operator->() const { return m_ptr; }
	bool isNull() const { return m_ptr == nullptr; }
};

Gdybyś próbował przekazać wartość l Auto_ptr5 do funkcji według wartości, kompilator narzekałby, że konstruktor kopiujący wymagany do inicjacji parametru funkcji został usunięty. To dobrze, ponieważ prawdopodobnie i tak powinniśmy przekazywać Auto_ptr5 przez odwołanie do wartości l const!

Auto_ptr5 jest (wreszcie) dobrą klasą inteligentnego wskaźnika. I tak naprawdę standardowa biblioteka zawiera klasę bardzo podobną do tej (której zamiast tego powinieneś używać) o nazwie std::unique_ptr. Porozmawiamy więcej o std::unique_ptr w dalszej części tego rozdziału.

Kolejny przykład

Przyjrzyjmy się innej klasie korzystającej z pamięci dynamicznej: prostej dynamicznej tablicy z szablonami. Ta klasa zawiera konstruktor kopiujący głęboko kopiujący i operator przypisania kopiowania.

#include <cstddef> // for std::size_t

template <typename T>
class DynamicArray
{
private:
	T* m_array {};
	int m_length {};

	void alloc(int length)
	{
		m_array = new T[static_cast<std::size_t>(length)];
        	m_length = length;
	}
public:
	DynamicArray(int length)
	{
		alloc(length);
	}

	~DynamicArray()
	{
		delete[] m_array;
	}

	// Copy constructor
	DynamicArray(const DynamicArray &arr)
	{
		alloc(arr.m_length);
		std::copy_n(arr.m_array, m_length, m_array); // copy m_length elements from arr to m_array
	}

	// Copy assignment
	DynamicArray& operator=(const DynamicArray &arr)
	{
		if (&arr == this)
			return *this;

		delete[] m_array;

		alloc(arr.m_length);

		std::copy_n(arr.m_array, m_length, m_array); // copy m_length elements from arr to m_array

		return *this;
	}

	int getLength() const { return m_length; }
	T& operator[](int index) { return m_array[index]; }
	const T& operator[](int index) const { return m_array[index]; }
};

Teraz użyjmy tej klasy w programie. Aby pokazać, jak ta klasa radzi sobie, gdy alokujemy milion liczb całkowitych na stercie, wykorzystamy klasę Timer, którą opracowaliśmy w lekcji 18.4 — Synchronizacja kodu. Użyjemy klasy Timer, aby zmierzyć szybkość działania naszego kodu i pokazać różnicę w wydajności pomiędzy kopiowaniem a przenoszeniem.

#include <algorithm> // for std::copy_n
#include <chrono> // for std::chrono functions
#include <iostream>

// Uses the above DynamicArray class

class Timer
{
private:
	// Type aliases to make accessing nested type easier
	using Clock = std::chrono::high_resolution_clock;
	using Second = std::chrono::duration<double, std::ratio<1> >;
	
	std::chrono::time_point<Clock> m_beg { Clock::now() };

public:
	void reset()
	{
		m_beg = Clock::now();
	}
	
	double elapsed() const
	{
		return std::chrono::duration_cast<Second>(Clock::now() - m_beg).count();
	}
};

// Return a copy of arr with all of the values doubled
DynamicArray<int> cloneArrayAndDouble(const DynamicArray<int> &arr)
{
	DynamicArray<int> dbl(arr.getLength());
	for (int i = 0; i < arr.getLength(); ++i)
		dbl[i] = arr[i] * 2;

	return dbl;
}

int main()
{
	Timer t;

	DynamicArray<int> arr(1000000);

	for (int i = 0; i < arr.getLength(); i++)
		arr[i] = i;

	arr = cloneArrayAndDouble(arr);

	std::cout << t.elapsed();
}

Na jednej z maszyn autora, w trybie wydania, program ten wykonał się w 0,00825559 sekundy.

Teraz zaktualizujemy DynamicArray zastępując konstruktor kopiujący i przypisanie kopiowania konstruktorem przenoszenia i przypisaniem przenoszenia, a następnie uruchom program ponownie:

#include <cstddef> // for std::size_t

template <typename T>
class DynamicArray
{
private:
	T* m_array {};
	int m_length {};

	void alloc(int length)
	{
		m_array = new T[static_cast<std::size_t>(length)];
		m_length = length;
	}
public:
	DynamicArray(int length)
	{
		alloc(length);
	}

	~DynamicArray()
	{
		delete[] m_array;
	}

	// Copy constructor
	DynamicArray(const DynamicArray &arr) = delete;

	// Copy assignment
	DynamicArray& operator=(const DynamicArray &arr) = delete;

	// Move constructor
	DynamicArray(DynamicArray &&arr) noexcept
		:  m_array { arr.m_array }, m_length { arr.m_length }
	{
		arr.m_length = 0;
		arr.m_array = nullptr;
	}

	// Move assignment
	DynamicArray& operator=(DynamicArray &&arr) noexcept
	{
		if (&arr == this)
			return *this;

		delete[] m_array;

		m_length = arr.m_length;
		m_array = arr.m_array;
		arr.m_length = 0;
		arr.m_array = nullptr;

		return *this;
	}

	int getLength() const { return m_length; }
	T& operator[](int index) { return m_array[index]; }
	const T& operator[](int index) const { return m_array[index]; }
};

#include <iostream>
#include <chrono> // for std::chrono functions

class Timer
{
private:
	// Type aliases to make accessing nested type easier
	using Clock = std::chrono::high_resolution_clock;
	using Second = std::chrono::duration<double, std::ratio<1> >;
	
	std::chrono::time_point<Clock> m_beg { Clock::now() };

public:
	void reset()
	{
		m_beg = Clock::now();
	}
	
	double elapsed() const
	{
		return std::chrono::duration_cast<Second>(Clock::now() - m_beg).count();
	}
};

// Return a copy of arr with all of the values doubled
DynamicArray<int> cloneArrayAndDouble(const DynamicArray<int> &arr)
{
	DynamicArray<int> dbl(arr.getLength());
	for (int i = 0; i < arr.getLength(); ++i)
		dbl[i] = arr[i] * 2;

	return dbl;
}

int main()
{
	Timer t;

	DynamicArray<int> arr(1000000);

	for (int i = 0; i < arr.getLength(); i++)
		arr[i] = i;

	arr = cloneArrayAndDouble(arr);

	std::cout << t.elapsed();
}

Na tej samej maszynie program ten został wykonany w 0,0056 sekundy.

Porównanie czasu wykonania dwóch programów, (0,00825559 - 0,0056) / 0,00825559 * 100 = 32,1% szybciej!

Usunięcie konstruktora ruchu i ruchu przypisanie

Możesz usunąć konstruktor przenoszenia i przenieść przypisanie, używając = delete składni dokładnie w ten sam sposób, w jaki możesz usunąć konstruktor kopiujący i przypisanie.

#include <iostream>
#include <string>
#include <string_view>

class Name
{
private:
    std::string m_name {};

public:
    Name(std::string_view name) : m_name{ name }
    {
    }

    Name(const Name& name) = delete;
    Name& operator=(const Name& name) = delete;
    Name(Name&& name) = delete;
    Name& operator=(Name&& name) = delete;

    const std::string& get() const { return m_name; }
};

int main()
{
    Name n1{ "Alex" };
    n1 = Name{ "Joe" }; // error: move assignment deleted

    std::cout << n1.get() << '\n';

    return 0;
}

Jeśli usuniesz konstruktor kopiujący, kompilator nie wygeneruje ukrytego konstruktora przenoszenia (co sprawi, że obiektów nie będzie można kopiować ani przenosić). Dlatego też podczas usuwania konstruktora kopiującego warto jasno określić, jakiego zachowania oczekujesz od konstruktorów przenoszenia. Usuń je jawnie (jasno, że jest to pożądane zachowanie) lub domyślnie je (uczyń klasę tylko przenoszeniem).

Kluczowa informacja

Klasa reguły pięciu mówi, że jeśli konstruktor kopiujący, przypisanie kopiujące, konstruktor przenoszenia, przypisanie przenoszenia lub destruktor są zdefiniowane lub usunięte, wówczas każda z tych funkcji powinna zostać zdefiniowana lub usunięta.

Usunięcie tylko konstruktora przenoszenia i przypisania przenoszenia może wydawać się dobrym pomysłem, jeśli chcesz mieć obiekt, który można kopiować, ale nie można przenosić, ma to tę niefortunną konsekwencję, że klasa nie może zostać zwrócona według wartości w przypadkach, gdy nie ma zastosowania obowiązkowe usuwanie kopii. Dzieje się tak, ponieważ usunięty konstruktor przenoszenia jest nadal zadeklarowaną funkcją i dlatego kwalifikuje się do rozpoznawania przeciążenia. Zwrócenie według wartości będzie faworyzowało usunięty konstruktor przenoszenia w stosunku do nieusuniętego konstruktora kopiującego. Ilustruje to następujący program:

#include <iostream>
#include <string>
#include <string_view>

class Name
{
private:
    std::string m_name {};

public:
    Name(std::string_view name) : m_name{ name }
    {
    }

    Name(const Name& name) = default;
    Name& operator=(const Name& name) = default;

    Name(Name&& name) = delete;
    Name& operator=(Name&& name) = delete;

    const std::string& get() const { return m_name; }
};

Name getJoe()
{
    Name joe{ "Joe" };
    return joe; // error: Move constructor was deleted
}

int main()
{
    Name n{ getJoe() };

    std::cout << n.get() << '\n';

    return 0;
}

Problemy z semantyką przenoszenia oraz std::swap Zaawansowane

W lekcji 21.12 — Przeciążanie operatora przypisania, wspomnieliśmy o idiomie kopiowania i zamiany. Kopiowanie i zamiana działają również w przypadku semantyki przenoszenia, co oznacza, że ​​możemy zaimplementować nasz konstruktor przenoszenia i przypisanie przenoszenia, zamieniając zasoby z obiektem, który zostanie zniszczony.

Ma to dwie zalety:

  • Trwały obiekt kontroluje teraz zasoby, które wcześniej były własnością umierającego obiektu (co było naszym głównym celem).
  • Umierający obiekt kontroluje teraz zasoby, które wcześniej były własnością trwałego obiektu. Kiedy umierający obiekt faktycznie umrze, może dokonać dowolnego rodzaju oczyszczenia tych zasobów.

Kiedy myślisz o wymianie, pierwszą rzeczą, która przychodzi na myśl jest zwykle std::swap(). Jednak implementacja konstruktora przenoszenia i przypisania przenoszenia za pomocą std::swap() jest problematyczna, ponieważ std::swap() wywołuje zarówno konstruktor przenoszenia, jak i przypisanie przenoszenia obiektów obsługujących przesuwanie. Spowoduje to nieskończony problem rekurencji.

Możesz to zobaczyć w następującym przykładzie:

#include <iostream>
#include <string>
#include <string_view>

class Name
{
private:
    std::string m_name {}; // std::string is move capable

public:
    Name(std::string_view name) : m_name{ name }
    {
    }

    Name(const Name& name) = delete;
    Name& operator=(const Name& name) = delete;

    Name(Name&& name) noexcept
    {
        std::cout << "Move ctor\n";

        std::swap(*this, name); // bad!
    }

    Name& operator=(Name&& name) noexcept
    {
        std::cout << "Move assign\n";

        std::swap(*this, name); // bad!

        return *this;
    }

    const std::string& get() const { return m_name; }
};

int main()
{
    Name n1{ "Alex" };   
    n1 = Name{"Joe"}; // invokes move assignment

    std::cout << n1.get() << '\n';
    
    return 0;
}

Wypisuje:

Move assign
Move ctor
Move ctor
Move ctor
Move ctor

I tak dalej… aż do przepełnienia stosu.

Możesz zaimplementować konstruktor przenoszenia i przypisanie przenoszenia za pomocą własnej funkcji wymiany, o ile funkcja elementu członkowskiego wymiany nie wywołuje konstruktora przenoszenia ani przypisania przenoszenia. Oto przykład, jak można to zrobić:

#include <iostream>
#include <string>
#include <string_view>

class Name
{
private:
    std::string m_name {};

public:
    Name(std::string_view name) : m_name{ name }
    {
    }

    Name(const Name& name) = delete;
    Name& operator=(const Name& name) = delete;
    
    // Create our own swap friend function to swap the members of Name
    friend void swap(Name& a, Name& b) noexcept
    {
        // We avoid recursive calls by invoking std::swap on the std::string member,
        // not on Name
        std::swap(a.m_name, b.m_name);
    }

    Name(Name&& name) noexcept
    {
        std::cout << "Move ctor\n";

        swap(*this, name); // Now calling our swap, not std::swap
    }

    Name& operator=(Name&& name) noexcept
    {
        std::cout << "Move assign\n";

        swap(*this, name); // Now calling our swap, not std::swap

        return *this;
    }

    const std::string& get() const { return m_name; }
};

int main()
{
    Name n1{ "Alex" };   
    n1 = Name{"Joe"}; // invokes move assignment

    std::cout << n1.get() << '\n';

    return 0;
}

Działa to zgodnie z oczekiwaniami i wypisuje:

Move assign
Joe
guest
Twój adres e-mail nie zostanie wyświetlony
Znalazłeś błąd? Zostaw komentarz powyżej!
Komentarze związane z poprawkami zostaną usunięte po przetworzeniu, aby pomóc zmniejszyć bałagan. Dziękujemy za pomoc w ulepszaniu witryny dla wszystkich!
Awatary z https://gravatar.com/ są połączone z podanym adresem e-mail.
Powiadamiaj mnie o odpowiedziach:  
426 Komentarze
Najnowsze
Najstarsze Najczęściej głosowane
Wbudowane opinie
Wyświetl wszystkie komentarze