Semantyka przenoszenia i r-wartości
Część sekcji Programowanie obiektowe ścieżki C++ w Coddy. Lekcja 83 z 104.
W C++ każde wyrażenie jest albo l-wartością (ma trwałą tożsamość, można pobrać jej adres), albo r-wartością (jest tymczasowe i zaraz zostanie zniszczone). Zrozumienie tej różnicy pozwala korzystać z semantyki przenoszenia — wydajnej optymalizacji, która pozwala uniknąć niepotrzebnego kopiowania.
Referencja r-wartości, deklarowana za pomocą &&, wiąże się konkretnie z obiektami tymczasowymi. Dzięki temu możesz „przejmować” zasoby z obiektów, które i tak zaraz znikną:
#include <iostream>
#include <utility>
class Buffer {
int* data;
size_t size;
public:
Buffer(size_t s) : data(new int[s]), size(s) {
std::cout << "Constructed\n";
}
// Konstruktor przenoszący – przejmuje zasoby
Buffer(Buffer&& other) noexcept
: data(other.data), size(other.size) {
other.data = nullptr; // Pozostaw obiekt źródłowy w poprawnym stanie
other.size = 0;
std::cout << "Moved\n";
}
~Buffer() { delete[] data; }
};
int main() {
Buffer b1(1000);
Buffer b2(std::move(b1)); // Wywołuje konstruktor przenoszący
}Funkcja std::move tak naprawdę niczego nie przenosi — po prostu rzutuje l-wartość na referencję do r-wartości, sygnalizując, że chcesz zrezygnować z zasobów obiektu. Faktyczne przenoszenie odbywa się w konstruktorze przenoszącym lub operatorze przypisania przenoszącego.
Semantyka przenoszenia znacznie poprawia wydajność podczas pracy z obiektami wymagającymi dużych zasobów, takimi jak kontenery czy ciągi znaków. Zamiast głęboko kopiować megabajty danych, po prostu przenosisz własność wskaźnika — operację o stałym czasie, niezależnie od rozmiaru.
Wyzwanie
ŁatwyStwórzmy klasę DataBuffer zarządzającą zasobami, która pokaże semantykę przenoszenia w działaniu. Zobaczysz, jak przenoszenie zasobów zamiast ich kopiowania może znacznie zwiększyć wydajność podczas przekazywania własności pamięci przydzielonej dynamicznie.
Rozmieścisz swój kod w trzech plikach:
DataBuffer.h: Zdefiniuj klasęDataBuffer, która zarządza dynamicznie przydzielaną tablicą liczb całkowitych.Klasa powinna mieć prywatne składowe przechowujące wskaźnik na dane (
int*), rozmiar (size_t) oraz nazwę (std::string), aby ułatwić rozpoznawanie poszczególnych buforów podczas operacji.Zadeklaruj:
- Konstruktor, który przyjmuje nazwę typu
std::stringi rozmiar typusize_t, przydziela tablicę i wyświetla:[name] constructed with size [size] - Konstruktor przenoszący, który przyjmuje referencję do wartości r-wartościowej, przejmuje zasoby i wyświetla:
[name] moved from [source_name](bufor, do którego przenoszone są zasoby, przejmuje nazwę źródła) - Destruktor, który wyświetla
[name] destroyed(lubempty destroyed, jeśli bufor został przeniesiony) - Metodę
getSize(), która zwraca bieżący rozmiar - Metodę
getName(), która zwraca nazwę bufora
Pamiętaj, aby oznaczyć konstruktor przenoszący jako
noexcepti pozostawić obiekt źródłowy w prawidłowym, pustym stanie (nullptr, rozmiar 0, nazwa "empty").- Konstruktor, który przyjmuje nazwę typu
DataBuffer.cpp: Zaimplementuj wszystkie metody zadeklarowane w pliku nagłówkowym. Gdy uruchomi się destruktor, usuwaj dane tylko wtedy, gdy wskaźnik nie jest pusty. Dołącz<iostream>, aby móc wyświetlać dane.main.cpp: Wczytaj dwa dane wejściowe:- Nazwę bufora (ciąg znaków)
- Rozmiar bufora (liczba całkowita)
Utwórz obiekt
DataBuffero podanej nazwie i rozmiarze. Następnie utwórz drugi bufor, przenosząc zasoby z pierwszego za pomocąstd::move(). Po przeniesieniu wyświetl stan obu buforów:Original: [name] size=[size]New: [name] size=[size]
Dołącz
<utility>, aby użyćstd::move.
Na przykład dla danych wejściowych Alpha i 100:
Alpha constructed with size 100
Alpha moved from Alpha
Original: empty size=0
New: Alpha size=100
Alpha destroyed
empty destroyedDla danych wejściowych Buffer i 50:
Buffer constructed with size 50
Buffer moved from Buffer
Original: empty size=0
New: Buffer size=50
Buffer destroyed
empty destroyedZwróć uwagę, że konstruktor przenoszący przekazuje własność przydzielonej pamięci bez kopiowania danych. Oryginalny bufor pozostaje pusty, ale prawidłowy, a gdy oba bufory zostaną zniszczone pod koniec programu, pamięć usuwa tylko ten, który nadal jest jej właścicielem.
Spróbuj swoich sił
#include <iostream>
#include <string>
#include <utility>
#include "DataBuffer.h"
int main() {
std::string name;
int size;
std::cin >> name;
std::cin >> size;
// TODO: Utwórz DataBuffer o podanej nazwie i rozmiarze
// TODO: Utwórz drugi bufor, przenosząc dane z pierwszego za pomocą std::move()
// TODO: Wypisz stan obu buforów:
// Oryginalny: [name] size=[size]
// Nowy: [name] size=[size]
return 0;
}
Ta lekcja zawiera krótki quiz. Zacznij lekcję, żeby na niego odpowiedzieć i śledzić swoje postępy.
Wszystkie lekcje w sekcji Programowanie obiektowe
1Podstawy programowania obiektowego
Pliki zewnętrzneBudowanie i kompilacja C++Pliki nagłówkowe i pliki źródłowePrzestrzenie nazw i zakresWprowadzenie do programowania obiektowego w C++Klasy a obiektyWskaźnik „this”Metody (funkcje składowe)Atrybuty (składowe danych)Podstawy konstruktorów i destruktorówPowtórzenie – prosty kalkulator4Właściwości klas
Elementy instancji a elementy statyczneGettery i setteryStałe funkcje składoweSłowo kluczowe mutableMetody i zmienne statyczneFunkcje i klasy zaprzyjaźnionePodsumowanie – menedżer konta bankowego7Dziedziczenie
Podstawy dziedziczeniaPoziomy dostępu w dziedziczeniuKolejność wywołań konstruktora i destruktoraNadpisywanie metodFunkcje wirtualne i VTableWielokrotne dziedziczenieDziedziczenie wirtualnePowtórzenie — hierarchia pracowników2Zarządzanie pamięcią
Pamięć stosu a stertyWskaźniki i referencjePamięć dynamiczna (new/delete)Inteligentne wskaźniki w C++RAII w C++Podsumowanie — menedżer tablicy dynamicznej5Hermetyzacja
Specyfikatory dostępu w C++Specyfikatory dostępu — szczegółowoUkrywanie informacjiStruktura a klasaKlasy zagnieżdżone i wewnętrznePodsumowanie — system ewidencji studentów8Polimorfizm
Polimorfizm czasu kompilacji i wykonaniaPrzeciążanie funkcjiFunkcje wirtualne — powtórkaCzysto wirtualne funkcjeKlasy abstrakcyjneProjektowanie interfejsów w C++Rzutowanie dynamiczne i RTTIPodsumowanie — kalkulator kształtów3Konstruktory i destruktory
Konstruktor domyślnyKonstruktor z parametramiKonstruktor kopiującyKonstruktor przenoszącyListy inicjalizacyjne konstruktoraKonstruktory delegująceDestruktor — szczegółowe omówienieZasada trzech / pięciu / zeraPodsumowanie — klasa String6Przeciążanie operatorów
Wprowadzenie do przeciążania operatorówPrzeciążanie operatorów arytmetycznychPrzeciążanie operatorów porównaniaOperatory strumieniowePrzeciążanie operatora przypisaniaPrzeciążanie operatorów [] i ()Operatory konwersji typówPodsumowanie — klasa Matrix9Szablony
Szablony funkcjiSzablony klasSpecjalizacja szablonówSzablony wariadycznePodstawy SFINAE i cech typówPodsumowanie — kontener generyczny12Nowoczesne funkcje C++
Semantyka przenoszenia i r-wartościDoskonałe przekazywanieWyrażenia lambda w szczegółachstd::function i std::bindconstexpr i constevalWiązania strukturalneoptional, variant, anyPoćwicz samodzielnie: Kompilator C++ online