Zgodnie z Twoim zapytaniem, poniżej przedstawiam rozwiązania wszystkich zadań z pliku Zadania-1.pdf
, bazując na dostarczonych źródłach.
Podobieństwa:
std::sort
, dzięki wspólnej konwencji nazewnictwa i semantyki (np. funkcje begin()
, end()
, size()
).Różnice:
std::vector<T>
(Wektor):
[]
.std::list<T>
(Lista dwukierunkowa):
std::list
są iteratorami dwukierunkowymi.std::deque<T>
(Dwustronna kolejka):
std::vector
a std::list
.std::vector
. Iteratory std::deque
są iteratorami random-access.std::vector
i std::list
nie spełniają wymagań jednocześnie.#include <string>
#include <vector>
#include <algorithm>
#include <iostream>
struct Student {
std::string m_surname;
std::string m_firstname;
Student(std::string_view surname, std::string_view firstname)
: m_surname(surname), m_firstname(firstname) {}
// Operator do wyświetlania dla ułatwienia
friend std::ostream& operator<<(std::ostream& os, const Student& s) {
return os << s.m_surname << ", " << s.m_firstname;
}
};
int main() {
std::vector<Student> students = {
Student("Kowalski", "Jan"),
Student("Nowak", "Anna"),
Student("Kowalski", "Piotr"),
Student("Wiśniewski", "Maria"),
Student("Nowak", "Zofia")
};
std::cout << "--- Sortowanie domyślne (rosnąco) ---" << std::endl;
// Lambda z flagą do odwracania kolejności
bool ascending_order = true; // Flaga do kontroli kolejności
auto compare_students = [ascending_order](const Student& s1, const Student& s2) {
// Porównujemy najpierw po nazwisku
if (s1.m_surname != s2.m_surname) {
return ascending_order ? (s1.m_surname < s2.m_surname) : (s1.m_surname > s2.m_surname);
}
// Jeśli nazwiska są takie same, porównujemy po imieniu
return ascending_order ? (s1.m_firstname < s2.m_firstname) : (s1.m_firstname > s2.m_firstname);
};
std::sort(students.begin(), students.end(), compare_students);
for (const auto& s : students) {
std::cout << s << std::endl;
}
std::cout << "\n--- Sortowanie odwrócone (malejąco) ---" << std::endl;
// Zmieniamy flagę na false, aby odwrócić kolejność.
// Musimy użyć nowej lambdy lub zmienić flagę przed jej utworzeniem,
// ponieważ 'ascending_order' jest przechwycone przez wartość.
bool descending_order = false;
auto compare_students_desc = [descending_order](const Student& s1, const Student& s2) {
if (s1.m_surname != s2.m_surname) {
return descending_order ? (s1.m_surname < s2.m_surname) : (s1.m_surname > s2.m_surname);
}
return descending_order ? (s1.m_firstname < s2.m_firstname) : (s1.m_firstname > s2.m_firstname);
};
std::sort(students.begin(), students.end(), compare_students_desc);
for (const auto& s : students) {
std::cout << s << std::endl;
}
return 0;
}
Omówienie: Lambda (compare_students
) jest domknięciem (closure
), które jest obiektem zwracanym przez wyrażenie lambda. Przechwytuje ona zmienną ascending_order
przez wartość ([ascending_order]
), co oznacza, że domknięcie przechowuje kopię tej zmiennej jako swoje pole składowe. To pole składowe jest inicjowane przez konstruktor domknięcia. Operator wywołania (operator()
) domknięcia porównuje dwa obiekty Student
– najpierw po nazwisku, a następnie po imieniu. Flaga ascending_order
pozwala dynamicznie decydować o kierunku sortowania. Domyślnie operator ()
w lambdach jest const
, co oznacza, że nie modyfikuje stanu domknięcia, chyba że użyjemy słowa kluczowego mutable
. W tym przypadku, ponieważ modyfikujemy tylko zwracaną wartość, a nie wewnętrzny stan lambdy, mutable
nie jest potrzebne. Funkcja std::sort
przyjmuje callable jako ostatni argument do niestandardowego porównywania elementów.
Używanie “surowych” wskaźników (raw pointers
) w C++ jest bardzo podatne na błędy. Główne problemy to:
Problem typu (the type problem
):
new
i delete
: dla pojedynczych obiektów oraz dla tablic (new T
i new T[]
, delete p
i delete[] p
).new
, muszą zostać usunięte odpowiadającą jej wersją delete
. Jednak typ wskaźnika (T*
) jest taki sam w obu przypadkach, co ułatwia niezgodność wersji (mismatch versions
). Skutkuje to niezdefiniowanym zachowaniem (undefined behavior
).Problem własności (the ownership problem
):
my
czy someone else
) jest odpowiedzialny za zniszczenie zaalokowanych danych. Prowadzi to do:
memory leak
): Gdy dynamicznie zaalokowane dane nigdy nie są niszczone.dangling pointer
): Gdy wskaźnik nadal wskazuje na miejsce w pamięci, z którego dane zostały już usunięte, a my próbujemy ich używać.double deletion
): Gdy próbujemy usunąć dane, które zostały już zniszczone, co również prowadzi do niezdefiniowanego zachowania.Problem obsługi wyjątków (the exception handling problem
):
Rozwiązanie: Standardowe wskaźniki inteligentne (smart pointers
) takie jak std::unique_ptr
i std::shared_ptr
zostały wprowadzone w C++11, aby rozwiązać te problemy, automatyzując zarządzanie pamięcią i zapewniając bezpieczeństwo w kontekście wyjątków.
Aby śledzić istnienie obiektu bez posiadania jego własności (co jest kluczowe, aby użytkownicy mogli samodzielnie decydować o jego czasie życia), użyjemy std::weak_ptr
.
#include <iostream>
#include <memory> // Dla std::shared_ptr i std::weak_ptr
// Klasa A, którą będziemy tworzyć
struct A {
A() {
std::cout << "ctor: A created." << std::endl;
}
~A() {
std::cout << "dtor: A destroyed." << std::endl;
}
void do_something() const {
std::cout << "A doing something." << std::endl;
}
};
// Funkcja fabryczna zwracająca shared_ptr do obiektu A
std::shared_ptr<A> factory_A() {
// Statyczny weak_ptr do śledzenia ostatnio utworzonego obiektu A
// Jest statyczny, aby jego stan był zachowany między wywołaniami funkcji
static std::weak_ptr<A> last_created_A;
// Próbujemy zablokować weak_ptr, aby uzyskać shared_ptr.
// Jeśli obiekt nadal istnieje, lock() zwróci shared_ptr; w przeciwnym razie nullptr.
std::shared_ptr<A> ptr = last_created_A.lock();
if (!ptr) { // Jeśli obiekt nie istnieje (lub nie został jeszcze utworzony)
std::cout << "Object A does not exist or expired. Creating a new one." << std::endl;
// Tworzymy nowy obiekt A za pomocą make_shared (zalecane)
ptr = std::make_shared<A>();
// Aktualizujemy weak_ptr, aby śledził ten nowy obiekt
last_created_A = ptr;
} else {
std::cout << "Object A still exists. Reusing it." << std::endl;
}
return ptr;
}
int main() {
{
std::cout << "--- First call ---" << std::endl;
std::shared_ptr<A> s_ptr1 = factory_A(); // Utworzy nowy obiekt
s_ptr1->do_something();
std::cout << "s_ptr1 count: " << s_ptr1.use_count() << std::endl;
std::cout << "\n--- Second call (object still alive) ---" << std::endl;
std::shared_ptr<A> s_ptr2 = factory_A(); // Użyje istniejącego obiektu
s_ptr2->do_something();
std::cout << "s_ptr1 count: " << s_ptr1.use_count() << std::endl;
std::cout << "s_ptr2 count: " << s_ptr2.use_count() << std::endl;
// Kiedy s_ptr1 i s_ptr2 wyjdą poza zakres, obiekt A zostanie zniszczony
// (ponieważ liczniki shared_ptr spadną do zera).
std::cout << "\nLeaving inner scope." << std::endl;
} // Obiekt A zostanie zniszczony tutaj, jeśli s_ptr1 i s_ptr2 były ostatnimi shared_ptr
std::cout << "\n--- Third call (object likely destroyed) ---" << std::endl;
std::shared_ptr<A> s_ptr3 = factory_A(); // Utworzy nowy obiekt, jeśli poprzedni został zniszczony
s_ptr3->do_something();
std::cout << "s_ptr3 count: " << s_ptr3.use_count() << std::endl;
std::cout << "\nEnd of main." << std::endl;
return 0;
}
Omówienie: Funkcja factory_A
wykorzystuje statyczny std::weak_ptr<A> last_created_A
do śledzenia instancji obiektu A
. std::weak_ptr
pozwala na obserwację obiektu zarządzanego przez std::shared_ptr
bez zwiększania licznika referencji tego shared_ptr
. Oznacza to, że weak_ptr
nie posiada własności obiektu i nie utrzymuje go przy życiu.
Kiedy factory_A
jest wywoływana:
std::shared_ptr
z last_created_A
za pomocą funkcji lock()
.lock()
zwróci niepusty shared_ptr
, oznacza to, że poprzednio utworzony obiekt A
nadal istnieje i jest zarządzany przez co najmniej jeden shared_ptr
. W takim przypadku funkcja zwraca ten istniejący shared_ptr
.lock()
zwróci pusty shared_ptr
(czyli weak_ptr
wygasł – expired()
), oznacza to, że obiekt A
został zniszczony (ponieważ wszystkie shared_ptr
do niego wyszły z zakresu) lub nigdy wcześniej nie został utworzony. W tej sytuacji funkcja tworzy nowy obiekt A
za pomocą std::make_shared<A>()
i aktualizuje last_created_A
, aby śledził ten nowo utworzony obiekt. Następnie zwraca nowy shared_ptr
.Dzięki temu mechanizmowi, funkcja factory_A
zawsze dostarcza działający shared_ptr
do obiektu A
, reusingc go, jeśli to możliwe, lub tworząc nowy, jeśli poprzedni już nie istnieje.
Konwencja wywołania funkcji (call convention
) odnosi się do technicznych szczegółów sposobu wywoływania funkcji, które zależą od platformy (architektury systemu, systemu operacyjnego i kompilatora). C++ nie specyfikuje jednej konwencji wywołania, ale niektóre jego funkcjonalności (takie jak elizja konstruktorów i optymalizacja wartości zwracanych) wynikają z typowych konwencji.
Typowo, konwencja wywołania funkcji wymaga, aby kod wywołujący funkcję (caller
):
Różnice między konwencjami:
legacy call convention
): Funkcja zwracała wynik w tymczasowym miejscu na szczycie stosu. Było to proste do zlokalizowania, ale miało wadę polegającą na konieczności skopiowania wyniku z tego tymczasowego miejsca do jego ostatecznego przeznaczenia. Oznaczało to, że zwracanie obiektów przez wartość wiązało się z dwukrotnym kopiowaniem.modern call convention
): Pozwala na alokację miejsca dla wartości zwracanej w dowolnym miejscu w pamięci (nie tylko na stosie, ale także na stercie lub w pamięci danych globalnych/statycznych). Adres tego miejsca jest przekazywany do funkcji, co umożliwia funkcji bezpośrednie utworzenie wartości zwracanej w docelowej lokalizacji. Eliminuje to potrzebę tymczasowego miejsca i związanych z tym kopii.Dzięki nowoczesnej konwencji wywołania i mechanizmom takim jak elizja konstruktorów (constructor elision
) oraz optymalizacja wartości zwracanej (return value optimization
- RVO), współczesne C++ sprawia, że przekazywanie parametrów i zwracanie wyników przez wartość jest bardzo wydajne, ponieważ obiekt nie jest niepotrzebnie kopiowany ani przenoszony. Elizja konstruktorów jest możliwa, ponieważ tymczasowy lub lokalny obiekt jest tworzony bezpośrednio w miejscu jego przeznaczenia.
#include <iostream>
#include <string>
#include <utility> // Dla std::move
void foo(int& val) {
std::cout << "foo(int&): Argument jest l-wartością modyfikowalną. Wartość: " << val << std::endl;
val = 100; // Możemy modyfikować
}
void foo(const int& val) {
std::cout << "foo(const int&): Argument jest l-wartością niemodyfikowalną lub r-wartością. Wartość: " << val << std::endl;
// val = 200; // Błąd: nie można modyfikować const referencji
}
void foo(int&& val) {
std::cout << "foo(int&&): Argument jest r-wartością. Wartość: " << val << std::endl;
val = 300; // Możemy modyfikować r-wartość, bo jest to referencja do tymczasowego obiektu
}
int main() {
int x = 10;
const int y = 20;
int z = 30;
std::cout << "--- Wywołania z dostępnymi wszystkimi przeciążeniami ---" << std::endl;
// foo(int&) będzie wybrane, ponieważ 'x' jest l-wartością niemodyfikowalną (non-const lvalue)
foo(x);
std::cout << "x po foo(x): " << x << std::endl; // x = 100
// foo(const int&) będzie wybrane, ponieważ 'y' jest l-wartością const (const lvalue)
foo(y);
// foo(int&&) będzie wybrane, ponieważ literal '40' jest r-wartością (prvalue)
foo(40);
// foo(int&&) będzie wybrane, ponieważ std::move(z) jawnie konwertuje 'z' do r-wartości (xvalue)
foo(std::move(z));
std::cout << "z po foo(std::move(z)): " << z << std::endl; // z = 300 (modyfikacja na tymczasowym)
std::cout << "\n--- Wywołania z ograniczoną dostępnością przeciążeń ---" << std::endl;
// Komentujemy foo(int&) i foo(int&&), aby zobaczyć zachowanie
// załóżmy, że dostępne jest tylko: void foo(const int&)
/*
// PRZYKŁAD 1: Tylko foo(const int&) jest dostępne
// Odkomentuj tylko to przeciążenie:
// void foo(const int& val) { std::cout << "foo(const int&)" << std::endl; }
//
// foo(x); // Wybierze foo(const int&), ponieważ const referencja może wiązać się z non-const l-wartością
// foo(y); // Wybierze foo(const int&), ponieważ const referencja może wiązać się z const l-wartością
// foo(40); // Wybierze foo(const int&), ponieważ const referencja może wiązać się z r-wartością
// foo(std::move(z)); // Wybierze foo(const int&), ponieważ const referencja może wiązać się z r-wartością
*/
/*
// PRZYKŁAD 2: Dostępne foo(int&) i foo(const int&)
// Odkomentuj tylko te przeciążenia:
// void foo(int& val) { std::cout << "foo(int&)" << std::endl; }
// void foo(const int& val) { std::cout << "foo(const int&)" << std::endl; }
//
// foo(x); // Wybierze foo(int&), ponieważ jest to lepsze dopasowanie dla non-const l-wartości
// foo(y); // Wybierze foo(const int&), ponieważ 'y' jest const l-wartością
// foo(40); // Błąd kompilacji, int& nie może wiązać się z r-wartością, const int& nie jest najlepszym dopasowaniem dla r-wartości gdyby była int&&
// foo(std::move(z)); // Błąd kompilacji, r-wartość nie może wiązać się z int&, a const int& nie jest najlepszym dopasowaniem dla r-wartości gdyby była int&&
*/
/*
// PRZYKŁAD 3: Dostępne foo(const int&) i foo(int&&)
// Odkomentuj tylko te przeciążenia:
// void foo(const int& val) { std::cout << "foo(const int&)" << std::endl; }
// void foo(int&& val) { std::cout << "foo(int&&)" << std::endl; }
//
// foo(x); // Wybierze foo(const int&), ponieważ 'x' jest l-wartością, a int&& nie może wiązać się z l-wartością
// foo(y); // Wybierze foo(const int&), ponieważ 'y' jest const l-wartością
// foo(40); // Wybierze foo(int&&), ponieważ jest to lepsze dopasowanie dla r-wartości
// foo(std::move(z)); // Wybierze foo(int&&), ponieważ jest to lepsze dopasowanie dla r-wartości
*/
return 0;
}
Omówienie wyboru przeciążeń (overload resolution
): Dla wyrażenia wywołania foo(<expr>)
, kompilator wybierze (overload resolution
) następujące przeciążenie:
void foo(T &)
: Wybierane, jeśli <expr>
jest l-wartością typu niemodyfikowalnego (non-const lvalue
). To jest najbardziej specyficzne dopasowanie dla modyfikowalnej l-wartości.void foo(const T &)
: Wybierane, jeśli <expr>
jest l-wartością typu const
(const lvalue
). Może również wiązać się z l-wartością typu niemodyfikowalnego lub z r-wartością, jeśli nie ma bardziej specyficznego dopasowania (tj. T&
lub T&&
). Jest to używane do przekazywania danych tylko do odczytu.void foo(T &&)
: Wybierane, jeśli <expr>
jest r-wartością. To jest najbardziej specyficzne dopasowanie dla r-wartości. Referencja r-wartościowa (T&&
) może wiązać się tylko z r-wartościami.Kluczowe punkty:
x
) jest zawsze l-wartością, nawet jeśli jej typem jest referencja r-wartościowa (np. int&& r = 10; r
jest l-wartością).std::move
jawnie konwertuje l-wartość na r-wartość (technicznie, zwraca referencję r-wartościową do l-wartości).Konwersja z l-wartości do r-wartości może odbywać się na dwa sposoby w C++: niejawnie (standardowa konwersja) oraz jawnie.
1. Niejawna (standardowa) konwersja (Implicit conversion
):
T
(niebędąca funkcją ani tablicą) może zostać niejawnie skonwertowana na r-wartość.+
dla typów całkowitych wymaga r-wartości jako swoich operandów. Jeśli podamy l-wartości x
i y
w wyrażeniu x + y
, zostaną one niejawnie skonwertowane na r-wartości.*
): Operator *
(dereferencji wskaźnika) wymaga wartości adresu pamięci, która jest r-wartością. Jeśli p
jest l-wartością wskaźnikową, *p
jest poprawne, ponieważ p
zostanie niejawnie skonwertowane na r-wartość.&
(adresu) wymaga l-wartości, a r-wartość nie zostanie do niej skonwertowana.2. Jawna konwersja (Explicit conversion
):
static_cast<T &&>(<expr>)
, gdzie <expr>
może być l-wartością lub r-wartością. Jednak jest to dość obszerne, ponieważ wymaga podania typu T
.std::move(<expr>)
.
std::move
jest funkcją z biblioteki standardowej, która, pomimo swojej nazwy, nie przenosi niczego. Zamiast tego, konwertuje swój argument na r-wartość (dokładniej, na r-wartościową referencję - xvalue
), co pozwala kompilatorowi wybrać odpowiednie przeciążenie (np. konstruktor przenoszący lub operator przypisania przenoszącego).T
na podstawie <expr>
, więc nie musimy go jawnie podawać.std::move
jest jawne włączenie semantyki przenoszenia dla obiektu, który domyślnie miałby włączoną semantykę kopiowania (ponieważ jest l-wartością).Struktura A (tylko do przenoszenia): Aby struktura była tylko do przenoszenia, jej konstruktor kopiujący i operator przypisania kopiującego muszą być usunięte (= delete
), a konstruktor przenoszący i operator przypisania przenoszącego muszą być jawnie domyślne (= default
).
#include <string>
#include <utility> // Dla std::move
#include <iostream>
// Struktura A: Typ tylko do przenoszenia
struct A {
std::string m_name;
// Domyślny konstruktor
A() = default;
// Konstruktor z argumentem (do inicjalizacji m_name)
A(std::string&& name) : m_name(std::move(name)) {
std::cout << "A::ctor: " << m_name << std::endl;
}
A(const std::string& name) : m_name(name) {
std::cout << "A::ctor (copy string): " << m_name << std::endl;
}
// Konstruktor kopiujący usunięty -> A jest 'copy-deleted'
A(const A&) = delete;
// Operator przypisania kopiującego usunięty -> A jest 'copy-deleted'
A& operator=(const A&) = delete;
// Jawnie domyślny konstruktor przenoszący
// To sprawia, że A jest typem tylko do przenoszenia
A(A&& other) = default; // std::cout << "A::move-ctor" << std::endl;
// Opcjonalnie można dodać customowy log, ale default działa jak należy
// Jawnie domyślny operator przypisania przenoszącego
A& operator=(A&& other) = default; // std::cout << "A::move-assign" << std::endl;
// Opcjonalnie można dodać customowy log, ale default działa jak należy
~A() {
std::cout << "A::dtor: " << m_name << std::endl;
}
};
// Struktura B: Pochodna od A. Jej składowe kopiujące powinny przenosić wartość obiektu bazowego.
struct B : A {
std::string m_data; // Dodatkowe pole składowe
// Domyślny konstruktor
B() = default;
// Konstruktor z argumentami
B(std::string_view name_a, std::string_view data_b)
: A(std::string(name_a)), m_data(data_b) {
std::cout << "B::ctor: " << m_name << ", " << m_data << std::endl;
}
// Konstruktor kopiujący dla B
// Zgodnie z wymaganiem, 'przenosi' wartość obiektu bazowego (A)
// To oznacza, że używa konstruktora przenoszącego A dla części bazowej.
// Jest to niestandardowe zachowanie dla konstruktora kopiującego.
B(const B& other)
: A(std::move(other)), // Przenosi część bazową A (wywołuje A::A(A&&))
m_data(other.m_data) // Kopiuje pole składowe m_data
{
std::cout << "B::copy-ctor (moves A base, copies m_data)" << std::endl;
// Ważne: `other` samo w sobie jest `const B&`, ale w kontekście
// `std::move(other)` staje się r-wartością, umożliwiając wywołanie
// konstruktora przenoszącego `A`. Po tym wywołaniu `other` (jako `A`)
// jest w stanie undefined, ale spójnym.
// Konsekwencja: `other.m_name` w `m_data(other.m_data)`
// może odczytać niezdefiniowany stan bazowej A.
// Poprawne podejście dla kopiowania bazowej A, która jest move-only,
// jest wyzwaniem i zazwyczaj wymaga specjalnego rozważenia.
// W typowym scenariuszu, jeśli bazowa klasa jest move-only,
// klasa pochodna również byłaby move-only.
// Ta implementacja spełnia *dosłowne* wymaganie "przenosić wartość obiektu bazowego",
// ale ma implikacje dla stanu `other` po konstrukcji.
}
// Operator przypisania kopiującego dla B
// Podobnie, przenosi wartość obiektu bazowego
B& operator=(const B& other) {
std::cout << "B::copy-assign (moves A base, copies m_data)" << std::endl;
A::operator=(std::move(other)); // Przenosi część bazową A
m_data = other.m_data; // Kopiuje pole składowe m_data
return *this;
}
// Konstruktor przenoszący i operator przypisania przenoszącego
// mogą być domyślne lub jawnie zdefiniowane, jeśli B miałoby być move-only.
// Tutaj nie są wymagane dla "składowe kopiujące powinny przenosić bazową",
// ale jeśli B miałoby być typem tylko do kopiowania, te powinny być usunięte.
// Dla tej demonstracji, możemy je usunąć, aby B było *tylko do kopiowania* w sensie braku przenoszenia.
B(B&&) = delete;
B& operator=(B&&) = delete;
~B() {
std::cout << "B::dtor: " << m_data << std::endl;
}
};
int main() {
std::cout << "--- Test A (move-only) ---" << std::endl;
A a1("Alpha");
A a2(std::move(a1)); // Przeniesienie (OK)
// A a3 = a1; // Błąd kompilacji: usunięty konstruktor kopiujący
std::cout << "\n--- Test B (copying members move A base) ---" << std::endl;
B b1("Base1", "Data1");
std::cout << "b1.m_name: " << b1.m_name << ", b1.m_data: " << b1.m_data << std::endl;
// Skopiuj b1 do b2
B b2 = b1; // Wywołuje B::copy-ctor
std::cout << "b2.m_name: " << b2.m_name << ", b2.m_data: " << b2.m_data << std::endl;
// Stan b1.m_name po skopiowaniu b2 (przez przeniesienie bazowej A) jest niezdefiniowany.
// b1.m_name może być teraz puste lub mieć zmienioną wartość.
std::cout << "b1.m_name AFTER B::copy-ctor (undefined state): " << b1.m_name << std::endl;
B b3("Base3", "Data3");
b3 = b2; // Wywołuje B::copy-assign
std::cout << "b3.m_name: " << b3.m_name << ", b3.m_data: " << b3.m_data << std::endl;
std::cout << "b2.m_name AFTER B::copy-assign (undefined state): " << b2.m_name << std::endl;
// B b4 = std::move(b1); // Błąd kompilacji: usunięty konstruktor przenoszący B
// b3 = std::move(b2); // Błąd kompilacji: usunięty operator przypisania przenoszącego B
std::cout << "\n--- End of main ---" << std::endl;
return 0;
}
Omówienie:
A
: Jest typem tylko do przenoszenia, ponieważ jej konstruktor kopiujący i operator przypisania kopiującego są jawnie usunięte (= delete
), a konstruktor przenoszący i operator przypisania przenoszącego są jawnie domyślne (= default
). Oznacza to, że obiekty A
mogą być przenoszone (np. za pomocą std::move
), ale nie kopiowane.B
: Jest dziedziczona z A
. Ma pole składowe m_data
. Kluczowe jest spełnienie wymagania, aby jej składowe kopiujące przenosiły wartość obiektu bazowego (A
).
B
(B(const B& other)
), inicjalizujemy część bazową A
za pomocą A(std::move(other))
. To wywołuje konstruktor przenoszący A
dla bazowego pod-obiektu other
, effectively “przenosząc” stan bazowej klasy z other
do *this
. Pole m_data
jest kopiowane tradycyjnie.B
(B& operator=(const B& other)
), używamy A::operator=(std::move(other))
do przeniesienia stanu bazowej klasy A
z other
do *this
. Pole m_data
jest kopiowane.other
) po operacji. Po “przeniesieniu” bazowej części A
z other
, stan other.m_name
jest niezdefiniowany, ale spójny. W większości praktycznych scenariuszy, jeśli klasa bazowa jest move-only
, klasa pochodna również byłaby move-only
lub miałaby bardzo specyficzne i dobrze udokumentowane zachowanie kopiujące. W tym przykładzie spełniamy dosłowne wymaganie zadania.C++ organizuje pamięć w celu optymalizacji wydajności i zapewnienia elastycznej kontroli nad zarządzaniem danymi. Głównymi miejscami alokacji danych w pamięci odczytu-zapisu (read-write memory
) są stos (stack
) i sterta (heap
).
1. Stos (Stack
):
scope
), w odwrotnej kolejności do ich utworzenia (FILO - first in, last out
).stack overflow
), proces jest przerywany.Data colocation
): Dane na stosie są upakowane razem zgodnie z kolejnością ich utworzenia, co oznacza, że powiązane dane są blisko siebie. To jest korzystne dla wydajności, ponieważ dane potrzebne przez procesor są bardziej prawdopodobne, że znajdują się już w pamięci podręcznej procesora (processor memory cache
), co znacznie przyspiesza dostęp do pamięci.2. Sterta (Heap
):
new
i delete
(lub za pomocą inteligentnych wskaźników).delete
(lub zarządzane przez inteligentne wskaźniki), aby uniknąć wycieków pamięci.new
rzuca wyjątek std::bad_alloc
.std::unique_ptr
, std::shared_ptr
) do zarządzania danymi na stercie, aby uniknąć problemów związanych z surowymi wskaźnikami.#include <iostream>
#include <string>
#include <utility> // Dla std::move
// Funkcja, która przyjmuje argument tylko przez referencję r-wartościową (rvalue reference)
void process_rvalue_only(std::string&& str) {
std::cout << "Funkcja process_rvalue_only została wywołana z r-wartością: " << str << std::endl;
// Możemy modyfikować str, ponieważ jest to referencja r-wartościowa, a nie const r-wartość
str = "Zmieniono w funkcji";
std::cout << "Po modyfikacji w funkcji: " << str << std::endl;
}
int main() {
std::string my_lvalue_string = "Hello World (l-wartość)";
std::cout << "Oryginalna l-wartość: " << my_lvalue_string << std::endl;
// Próba wywołania funkcji z l-wartością bezpośrednio – to się nie skompiluje
// process_rvalue_only(my_lvalue_string); // Błąd kompilacji: nie można wiązać rvalue reference do lvalue
// Jawna konwersja l-wartości na r-wartość za pomocą std::move
// std::move(my_lvalue_string) konwertuje my_lvalue_string na r-wartość (dokładnie xvalue)
// Co pozwala na wywołanie funkcji process_rvalue_only(std::string&&)
process_rvalue_only(std::move(my_lvalue_string));
// Po przeniesieniu, stan my_lvalue_string jest niezdefiniowany, ale spójny.
// Często jest puste, ale nie można na tym polegać.
std::cout << "Stan oryginalnej l-wartości po przeniesieniu (niezdefiniowany): " << my_lvalue_string << std::endl;
// Możemy również wywołać funkcję z r-wartością, która jest tworzona bezpośrednio
process_rvalue_only(std::string("Jestem r-wartością tymczasową"));
return 0;
}
Omówienie: Funkcja process_rvalue_only
jest zadeklarowana tak, aby przyjmować argument typu std::string&&
. Jest to referencja r-wartościowa, która może wiązać się tylko z r-wartościami (takimi jak literale numeryczne, wyniki funkcji zwracających przez wartość, czy tymczasowe obiekty). Bezpośrednie przekazanie l-wartości (np. my_lvalue_string
) do tej funkcji spowodowałoby błąd kompilacji, ponieważ referencja r-wartościowa nie może wiązać się z l-wartością.
Aby to obejść i jawnie skonwertować l-wartość (my_lvalue_string
) na r-wartość, używamy std::move(my_lvalue_string)
. std::move
jest funkcją szablonową, która konwertuje swój argument na r-wartość (technicznie, zwraca xvalue
, czyli r-wartościową referencję do obiektu). Ta konwersja umożliwia dopasowanie wywołania process_rvalue_only(std::move(my_lvalue_string))
do przeciążenia process_rvalue_only(std::string&&)
. Ważne jest, że po operacji przeniesienia (która następuje, jeśli typ std::string
ma zaimplementowany konstruktor przenoszący), stan oryginalnej zmiennej my_lvalue_string
jest niezdefiniowany, ale spójny.
Wyrażenie lambda (w skrócie “lambda”) jest syntaktycznym cukrem (syntactic sugar
) służącym do wygodnego tworzenia funktorów (functors
). Umożliwiają one tworzenie callable obiektów w bardziej zwięzły sposób w porównaniu do ręcznego tworzenia klas funktorów. Domknięcie (closure
) to funktor, który jest wynikiem wyrażenia lambda.
Kluczowe aspekty wyrażeń lambda:
Cel: Upraszczają pisanie kodu, zwłaszcza tam, gdzie potrzebna jest krótka, jednorazowa funkcja, np. jako predykat w algorytmach standardowych (jak std::sort
). Mimo że można by osiągnąć tę samą funkcjonalność za pomocą klas funktorów, lambdy są po prostu poręczne i mniej podatne na błędy.
Domknięcie (Closure
): Wyrażenie lambda tworzy typ funktora (strukturę lub klasę) i obiekt tego typu. Ten obiekt nazywany jest domknięciem. Typ domknięcia jest zazwyczaj anonimowy.
Składnia (Syntax
): Najczęstsza składnia wyrażenia lambda to:
[capture list](parameter list) mutable(optional) {body}
capture list
) []
: Określa, w jaki sposób zmienne z zakresu (scope) wyrażenia lambda są dostępne w jego ciele. Może być pusta.
=
: Domyślne przechwytywanie przez wartość. Wszystkie zmienne używane w ciele lambdy są przechwytywane przez wartość.&
: Domyślne przechwytywanie przez referencję. Wszystkie zmienne używane w ciele lambdy są przechwytywane przez referencję.[x, &y]
(x przez wartość, y przez referencję). Można łączyć domyślne przechwytywanie z jawnym nadpisaniem, np. [=, &y]
(domyślnie przez wartość, y
przez referencję).[a = x]
.parameter list
) ()
: Definiuje parametry, które są przekazywane do operatora wywołania funktorów. Działa jak lista parametrów zwykłej funkcji. Jeśli lista jest pusta, ()
można pominąć.mutable
(opcjonalnie): Domyślnie operator ()
w domknięciu jest const
. Użycie mutable
sprawia, że operator ()
staje się non-const
, co pozwala na modyfikowanie przechwyconych zmiennych przechwyconych przez wartość wewnątrz lambdy.body
) {}
: Zawiera kod, który zostanie wykonany po wywołaniu domknięcia. Nawet jeśli ciało jest puste, nawiasy {}
nie mogą być pominięte. Typ zwracany jest dedukowany na podstawie instrukcji return
w ciele; jeśli nie ma return
, typem jest void
.Przechowywanie danych: W przeciwieństwie do wskaźników na funkcje, lambdy (będące funktorami) mogą przechowywać dodatkowe dane (przechwycone zmienne) jako pola składowe.
Prostota: Najprostsze wyrażenie lambda to []{}
.
#include <iostream>
#include <string>
#include <vector>
#include <list>
#include <memory> // Dla std::shared_ptr, std::weak_ptr, std::make_shared
int main() {
// 1. Tworzenie dynamicznie dwóch ciągów znaków i umieszczanie ich w shared_ptr
// Używamy std::make_shared dla optymalnej alokacji pamięci
std::shared_ptr<std::string> hello_sptr = std::make_shared<std::string>("Hello");
std::shared_ptr<std::string> world_sptr = std::make_shared<std::string>("World");
// 2. Umieszczanie obiektów shared_ptr w wektorze
std::vector<std::shared_ptr<std::string>> sptr_vector;
sptr_vector.push_back(hello_sptr); // Kopiowanie shared_ptr - licznik referencji rośnie
sptr_vector.push_back(world_sptr);
std::cout << "--- Zawartość wektora shared_ptr ---" << std::endl;
for (const auto& sptr : sptr_vector) {
if (sptr) { // Sprawdź, czy shared_ptr nie jest pusty
std::cout << "Wartość: \"" << *sptr << "\", Licznik referencji: " << sptr.use_count() << std::endl;
}
}
// 3. Umieszczanie obiektów weak_ptr w liście
std::list<std::weak_ptr<std::string>> wptr_list;
// weak_ptr jest tworzony z shared_ptr
wptr_list.push_back(std::weak_ptr<std::string>(hello_sptr));
wptr_list.push_back(std::weak_ptr<std::string>(world_sptr));
std::cout << "\n--- Zawartość listy weak_ptr (przed zniszczeniem shared_ptr) ---" << std::endl;
for (const auto& wptr : wptr_list) {
if (!wptr.expired()) { // Sprawdź, czy obiekt nadal istnieje
// Aby bezpiecznie użyć danych, należy stworzyć shared_ptr z weak_ptr
std::shared_ptr<std::string> temp_sptr = wptr.lock();
if (temp_sptr) {
std::cout << "Wartość: \"" << *temp_sptr << "\", Licznik referencji (przez temp_sptr): " << temp_sptr.use_count() << std::endl;
}
} else {
std::cout << "Obiekt wygasł." << std::endl;
}
}
// Przykład pokazujący, co się dzieje, gdy shared_ptr wygaśnie
// Usuńmy jeden shared_ptr z wektora (np. "Hello")
// Jeśli hello_sptr (i jego kopie w wektorze) są ostatnimi shared_ptr, obiekt "Hello" zostanie zniszczony.
sptr_vector.erase(sptr_vector.begin()); // Usuwa pierwszy element ("Hello" shared_ptr)
std::cout << "\n--- Zawartość listy weak_ptr (po zniszczeniu jednego shared_ptr) ---" << std::endl;
for (const auto& wptr : wptr_list) {
if (!wptr.expired()) {
std::shared_ptr<std::string> temp_sptr = wptr.lock();
if (temp_sptr) {
std::cout << "Wartość: \"" << *temp_sptr << "\", Licznik referencji (przez temp_sptr): " << temp_sptr.use_count() << std::endl;
}
} else {
std::cout << "Obiekt wygasł (np. \"Hello\" jeśli jego shared_ptr został usunięty)." << std::endl;
}
}
return 0;
}
Omówienie:
std::shared_ptr
: Odpowiada za współdzielone posiadanie (shared ownership
) dynamicznie alokowanych danych. Gdy shared_ptr
jest kopiowany (np. do wektora), licznik referencji (use_count()
) jest zwiększany. Obiekt zarządzany jest niszczony, gdy ostatni shared_ptr
do niego zostanie zniszczony (np. wyjdzie z zakresu). std::make_shared
jest zalecanym sposobem tworzenia shared_ptr
, ponieważ optymalizuje alokację pamięci (alokuje dane i strukturę kontrolną w jednym kawałku).std::weak_ptr
: Pozwala na śledzenie zarządzanych danych bez przejmowania własności. Oznacza to, że weak_ptr
nie zwiększa licznika referencji shared_ptr
i nie utrzymuje obiektu przy życiu. Jest to przydatne do rozwiązywania problemów z cyklicznymi referencjami lub do sprawdzania, czy obiekt nadal istnieje.weak_ptr
: weak_ptr
nie pozwala na bezpośredni dostęp do zarządzanych danych (nie ma operatorów *
ani ->
). Aby bezpiecznie uzyskać dostęp do danych, należy najpierw “zablokować” weak_ptr
za pomocą funkcji lock()
, która próbuje utworzyć shared_ptr
. Jeśli obiekt, na który wskazywał weak_ptr
, już nie istnieje (expired()
jest true
), lock()
zwróci pusty shared_ptr
. W przeciwnym razie zwróci prawidłowy shared_ptr
, który tymczasowo zwiększy licznik referencji, zapewniając, że obiekt nie zostanie zniszczony podczas jego używania.W C++ kontenery oferują różne metody dodawania elementów, z których kluczowe to kopiowanie (copy
), przenoszenie (move
) oraz umieszczanie (emplace
). “Wstawianie” jest ogólnym terminem na dodawanie elementów, a “umieszczanie” jest jego specyficzną i zoptymalizowaną formą.
Kopiowanie (Copy
):
std::string s = "Hello";
), a następnie przekazujesz go do kontenera.copy constructor
) lub operator przypisania kopiującego (copy assignment operator
), aby utworzyć kopię obiektu w swoim wewnętrznym buforze.Przenoszenie (Move
):
std::move()
na obiekcie źródłowym, aby wskazać, że jego wartość może zostać “przeniesiona”, a nie skopiowana.move constructor
) lub operator przypisania przenoszącego (move assignment operator
) obiektu.Umieszczanie (Emplace
):
emplace
przyjmuje argumenty konstruktora elementu.in-place
), tj. bezpośrednio w wymaganej lokalizacji pamięci wewnętrznej kontenera. To oznacza, że nie ma żadnego niepotrzebnego kopiowania ani przenoszenia tymczasowych obiektów.emplace
(np. emplace_back
, emplace_front
, emplace
) wiążą się z takimi samymi problemami wydajnościowymi jak insert
(wstawianie), jeśli wymagane jest przesuwanie elementów w celu zachowania spójności kontenera (np. ciągłości pamięci w wektorze).Podsumowując, umieszczanie (emplace
) to zoptymalizowana forma wstawiania (insert
), która dąży do konstruowania obiektów bezpośrednio w kontenerze, unikając kosztów związanych z kopiowaniem lub przenoszeniem.
#include <vector>
#include <algorithm> // Dla std::sort
#include <iostream>
int main() {
std::vector<int> numbers = {3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5};
std::cout << "Original vector: ";
for (int n : numbers) {
std::cout << n << " ";
}
std::cout << std::endl;
// Sortowanie malejące za pomocą std::sort i wyrażenia lambda
// std::sort przyjmuje callable jako trzeci argument, który definiuje porządek.
// Lambda (funktor) jest używana do niestandardowego porównywania.
// Dla sortowania malejącego, chcemy, aby 'a' było "mniejsze" od 'b', jeśli 'a' jest większe od 'b'.
// Czyli, funkcja porównująca powinna zwracać true, jeśli pierwszy element powinien iść przed drugim.
// Aby posortować malejąco, dla a i b, jeśli a > b, to a powinno iść przed b.
std::sort(numbers.begin(), numbers.end(), [](int a, int b) {
return a > b; // Zwraca true, jeśli a powinno być przed b (dla sortowania malejącego)
});
std::cout << "Sorted descending: ";
for (int n : numbers) {
std::cout << n << " ";
}
std::cout << std::endl;
return 0;
}
Omówienie: Funkcja std::sort
jest algorytmem standardowym, który może być używany z różnymi kontenerami dzięki iteratorom. Domyślnie używa ona operatora <
do porównywania elementów. Aby zmienić kolejność sortowania na malejącą, należy dostarczyć niestandardową funkcję porównującą jako trzeci argument std::sort
.
W tym przykładzie używamy wyrażenia lambda [](int a, int b) { return a > b; }
jako funkcji porównującej. Ta lambda jest callable. Jest to funktor, który nie przechowuje żadnych danych. Gdy std::sort
wywołuje tę lambdę dla dwóch elementów a
i b
, zwraca true
, jeśli a
jest większe od b
. Oznacza to, że a
powinno być umieszczone przed b
w posortowanym zbiorze, co efektywnie sortuje wektor w kolejności malejącej.
Zarówno std::shared_ptr
, jak i std::weak_ptr
używają wspólnej struktury kontrolnej danych (control data structure
) do zarządzania cyklem życia alokowanych dynamicznie danych (managed data
).
Struktura Kontrolna Danych:
std::shared_ptr
, który przejmuje własność zarządzanych danych.std::shared_ptr
i std::weak_ptr
posiada wskaźnik do tej wspólnej struktury kontrolnej.Pola w strukturze kontrolnej:
Licznik referencji (Reference Count
):
std::shared_ptr
wskazujących na zarządzane dane.std::shared_ptr
jest kopiowany, licznik referencji jest inkrementowany.std::shared_ptr
jest niszczony (np. wychodzi z zakresu), licznik referencji jest dekrementowany.std::shared_ptr
posiadających własność tych danych.Licznik słabych referencji (Weak Count
):
std::weak_ptr
śledzących zarządzane dane.std::weak_ptr
nie zwiększają licznika referencji, co oznacza, że nie posiadają własności obiektu i nie utrzymują go przy życiu.weak_ptr
może nadal istnieć i śledzić obiekt, nawet jeśli sam obiekt zarządzany został już zniszczony. Struktura kontrolna musi istnieć, aby weak_ptr
mógł sprawdzić, czy obiekt jest nadal dostępny (np. za pomocą expired()
lub lock()
).Wydajność:
shared_ptr
zajmuje dwa razy więcej pamięci niż surowy wskaźnik, ponieważ przechowuje wskaźnik do zarządzanych danych i wskaźnik do struktury kontrolnej.weak_ptr
również zajmuje dwa razy więcej pamięci niż surowy wskaźnik, przechowując wskaźnik do danych i do struktury kontrolnej.#include <iostream>
#include <utility> // Dla std::move
template<typename T>
class my_unique_ptr {
private:
T* m_ptr; // Surowy wskaźnik do zarządzanych danych
public:
// 1. Konstruktor
// Akceptuje surowy wskaźnik i przejmuje jego własność.
// explicit, aby zapobiec niejawnej konwersji
explicit my_unique_ptr(T* ptr = nullptr) noexcept : m_ptr(ptr) {
std::cout << "my_unique_ptr::ctor" << std::endl;
}
// 2. Składowe specjalne
// Destruktor
// Odpowiada za zniszczenie zarządzanych danych, gdy my_unique_ptr wyjdzie z zakresu.
~my_unique_ptr() noexcept {
std::cout << "my_unique_ptr::dtor (deleting data)" << std::endl;
delete m_ptr; // Używamy delete dla pojedynczego obiektu
}
// Konstruktor kopiujący - usunięty
// unique_ptr ma wyłączną własność, więc nie można go kopiować
my_unique_ptr(const my_unique_ptr&) = delete;
// Operator przypisania kopiującego - usunięty
// unique_ptr ma wyłączną własność, więc nie można go kopiować
my_unique_ptr& operator=(const my_unique_ptr&) = delete;
// Konstruktor przenoszący
// Pozwala na przeniesienie własności z jednego my_unique_ptr do drugiego
my_unique_ptr(my_unique_ptr&& other) noexcept : m_ptr(other.m_ptr) {
std::cout << "my_unique_ptr::move_ctor" << std::endl;
other.m_ptr = nullptr; // Po przeniesieniu, źródło nie zarządza już danymi
}
// Operator przypisania przenoszącego
// Pozwala na przeniesienie własności poprzez przypisanie
my_unique_ptr& operator=(my_unique_ptr&& other) noexcept {
std::cout << "my_unique_ptr::move_assign" << std::endl;
if (this != &other) { // Zapobieganie samoznaczeniu
delete m_ptr; // Usuń aktualnie zarządzane dane przed przejęciem nowych
m_ptr = other.m_ptr;
other.m_ptr = nullptr;
}
return *this;
}
// 3. Funkcja get
// Zwraca surowy wskaźnik do zarządzanych danych.
T* get() const noexcept {
return m_ptr;
}
// Dodatkowa funkcjonalność (operator-> i operator*) dla wygody
T* operator->() const noexcept {
return m_ptr;
}
T& operator*() const noexcept {
return *m_ptr;
}
// Sprawdzanie, czy wskaźnik zarządza danymi
explicit operator bool() const noexcept {
return m_ptr != nullptr;
}
// Funkcja reset - przydatna do zarządzania nowymi danymi
void reset(T* ptr = nullptr) noexcept {
T* old_ptr = m_ptr;
m_ptr = ptr;
delete old_ptr; // Usuń stare dane
}
// Funkcja release - zwalnia własność bez niszczenia danych
T* release() noexcept {
T* old_ptr = m_ptr;
m_ptr = nullptr;
return old_ptr;
}
};
struct MyData {
int id;
MyData(int i) : id(i) { std::cout << "MyData(" << id << ") ctor" << std::endl; }
~MyData() { std::cout << "MyData(" << id << ") dtor" << std::endl; }
void print() { std::cout << "Data ID: " << id << std::endl; }
};
int main() {
std::cout << "--- Creating my_unique_ptr instances ---" << std::endl;
my_unique_ptr<MyData> ptr1(new MyData(1)); // Konstruktor
if (ptr1) {
ptr1->print();
}
std::cout << "\n--- Moving ownership ---" << std::endl;
my_unique_ptr<MyData> ptr2 = std::move(ptr1); // Konstruktor przenoszący
if (ptr2) {
ptr2->print();
}
if (!ptr1) { // ptr1 jest teraz pusty
std::cout << "ptr1 is now empty." << std::endl;
}
std::cout << "\n--- Assigning ownership ---" << std::endl;
my_unique_ptr<MyData> ptr3;
ptr3.reset(new MyData(3)); // Tworzy nowy obiekt i przypisuje do ptr3, stary (nullptr) jest usuwany
ptr3 = std::move(ptr2); // Operator przypisania przenoszącego (obiekt 3 zostanie usunięty, 2 przejmie)
if (ptr3) {
ptr3->print(); // Powinno być MyData(2)
}
std::cout << "\n--- Using get() ---" << std::endl;
MyData* raw_ptr = ptr3.get();
if (raw_ptr) {
raw_ptr->print();
}
std::cout << "\n--- End of main, ptr3 goes out of scope ---" << std::endl;
return 0;
}
Omówienie: Implementacja my_unique_ptr
naśladuje zachowanie std::unique_ptr
w kluczowych aspektach:
Exclusive Ownership
): my_unique_ptr
jest typem tylko do przenoszenia (move-only
). Oznacza to, że jego konstruktor kopiujący i operator przypisania kopiującego są jawnie usunięte (= delete
). Zapobiega to przypadkowemu kopiowaniu wskaźnika i problemom z podwójnym usunięciem lub wiszącymi wskaźnikami.~my_unique_ptr()
jest odpowiedzialny za wywołanie delete
na zarządzanym wskaźniku m_ptr
. Gwarantuje to, że pamięć zostanie zwolniona automatycznie, gdy obiekt my_unique_ptr
wyjdzie z zakresu.my_unique_ptr
do drugiego. Po przeniesieniu, wskaźnik źródłowy (other.m_ptr
) jest ustawiany na nullptr
, aby zapobiec podwójnemu usunięciu.get()
: Zwraca surowy wskaźnik do zarządzanych danych, co pozwala na bezpośredni dostęp do nich, ale z zachowaniem świadomości, że własność należy do my_unique_ptr
.Ten temat jest szczegółowo omówiony w rozwiązaniu zadania numer 6. Proszę odnieść się do niego, aby uzyskać pełne wyjaśnienie i przykłady.
W skrócie: Przeciążanie funkcji w C++ może zależeć od kategorii wartości argumentu wywołania, w tym od tego, czy argument jest l-wartością (lvalue
) czy r-wartością (rvalue
), oraz czy jest const
czy non-const
. Kompilator używa zasad rozstrzygania przeciążeń (overload resolution
) do wyboru najbardziej odpowiedniego przeciążenia. Typowe przeciążenia referencyjne to:
void foo(T &)
: Wiąże się z l-wartościami niemodyfikowalnymi.void foo(const T &)
: Wiąże się z l-wartościami (const i non-const) oraz r-wartościami. Jest to “catch-all” dla odczytu.void foo(T &&)
: Wiąże się tylko z r-wartościami. Jest to kluczowe dla semantyki przenoszenia.Kompilator wybierze najbardziej specyficzne dopasowanie. Na przykład, T&
będzie preferowane dla non-const lvalue
nad const T&
. T&&
będzie preferowane dla rvalue
nad const T&
.
To zadanie jest bardzo podobne do zadania numer 8, ale z precyzowaną strukturą A
i wymaganiami dla B
.
1. Zmiana struktury A, żeby była typem tylko do przenoszenia: Obecna A
już ma usunięty konstruktor kopiujący. Aby była tylko do przenoszenia, musi mieć jawnie zadeklarowane (lub domyślne) składowe przenoszące.
#include <string>
#include <utility> // Dla std::move
#include <iostream>
struct A {
std::string m_name; // Dodano pole składowe
A() = default; // Domyślny konstruktor
// Konstruktor kopiujący - usunięty (jak w zadaniu)
A(const A&) = delete;
// Operator przypisania kopiującego - usunięty (dla spójności)
A& operator=(const A&) = delete;
// Jawnie domyślny konstruktor przenoszący
// Ważne dla bycia typem tylko do przenoszenia
A(A&& other) noexcept = default;
// Jawnie domyślny operator przypisania przenoszącego
// Ważne dla bycia typem tylko do przenoszenia
A& operator=(A&& other) noexcept = default;
// Opcjonalne: konstruktor z argumentem, żeby było co testować
A(std::string&& name) : m_name(std::move(name)) {
std::cout << "A::ctor from rvalue string: " << m_name << std::endl;
}
A(const std::string& name) : m_name(name) {
std::cout << "A::ctor from lvalue string: " << m_name << std::endl;
}
~A() {
std::cout << "A::dtor: " << m_name << std::endl;
}
};
2. Implementacja struktury B: Będzie pochodzić od A, mieć pole m_txt
typu std::string
. Musi być typem tylko do kopiowania (bez przenoszenia), gdzie wartość obiektu bazowego (A
) będzie przenoszona, a składowego (m_txt
) kopiowana.
// Struktura B: Typ tylko do kopiowania, dziedziczy z A
struct B : A {
std::string m_txt; // Pole składowe
B() = default; // Domyślny konstruktor
// Konstruktor kopiujący B:
// Wartość obiektu bazowego (A) będzie przenoszona: A(std::move(other))
// Składowa m_txt będzie kopiowana: m_txt(other.m_txt)
B(const B& other)
: A(std::move(other)), // Wywołuje A::A(A&&)
m_txt(other.m_txt) // Wywołuje std::string::string(const std::string&)
{
std::cout << "B::copy-ctor: (A-base moved, m_txt copied)" << std::endl;
// Po tym wywołaniu, bazowa część obiektu 'other' jest w stanie niezdefiniowanym
// co oznacza, że other.m_name może być puste lub zmienione.
}
// Operator przypisania kopiującego B:
// Wartość obiektu bazowego (A) będzie przenoszona: A::operator=(std::move(other))
// Składowa m_txt będzie kopiowana: m_txt = other.m_txt
B& operator=(const B& other) {
std::cout << "B::copy-assign: (A-base moved, m_txt copied)" << std::endl;
if (this != &other) {
A::operator=(std::move(other)); // Wywołuje A::operator=(A&&)
m_txt = other.m_txt; // Wywołuje std::string::operator=(const std::string&)
}
return *this;
}
// Aby B było typem TYLKO DO KOPIOWANIA (bez przenoszenia),
// konstruktor przenoszący i operator przypisania przenoszącego muszą być usunięte.
B(B&&) = delete;
B& operator=(B&&) = delete;
// Opcjonalne: konstruktor z argumentami
B(std::string_view name_a, std::string_view text_b)
: A(std::string(name_a)), m_txt(text_b) {
std::cout << "B::ctor from strings: " << m_name << ", " << m_txt << std::endl;
}
~B() {
std::cout << "B::dtor: " << m_name << ", " << m_txt << std::endl;
}
};
int main() {
std::cout << "--- Test A (move-only) ---" << std::endl;
A a1("Initial A");
A a2(std::move(a1)); // OK: move-ctor
// A a3 = a1; // ERROR: copy-ctor deleted
std::cout << "\n--- Test B (copy-only, A-base moved, m_txt copied) ---" << std::endl;
B b1("BaseName1", "DataTxt1");
std::cout << "b1: (A::m_name = " << b1.m_name << ", B::m_txt = " << b1.m_txt << ")" << std::endl;
B b2 = b1; // OK: B::copy-ctor
std::cout << "b2 (copy of b1): (A::m_name = " << b2.m_name << ", B::m_txt = " << b2.m_txt << ")" << std::endl;
std::cout << "b1 after copy to b2 (A-base moved): (A::m_name = " << b1.m_name << ", B::m_txt = " << b1.m_txt << ")" << std::endl;
B b3("BaseName3", "DataTxt3");
std::cout << "b3 before assign: (A::m_name = " << b3.m_name << ", B::m_txt = " << b3.m_txt << ")" << std::endl;
b3 = b2; // OK: B::copy-assign
std::cout << "b3 after assign from b2: (A::m_name = " << b3.m_name << ", B::m_txt = " << b3.m_txt << ")" << std::endl;
std::cout << "b2 after assign to b3 (A-base moved): (A::m_name = " << b2.m_name << ", B::m_txt = " << b2.m_txt << ")" << std::endl;
// B b4 = std::move(b1); // ERROR: B::move-ctor deleted
// b3 = std::move(b1); // ERROR: B::move-assign deleted
std::cout << "\n--- End of main ---" << std::endl;
return 0;
}
Omówienie:
A
: Zgodnie z instrukcją, jest skonfigurowana jako typ tylko do przenoszenia. Osiąga się to poprzez usunięcie konstruktora kopiującego i operatora przypisania kopiującego oraz jawne domyślne zdefiniowanie konstruktora przenoszącego i operatora przypisania przenoszącego.B
:
A
.= delete
). Wymaga to jawnego zdefiniowania konstruktora kopiującego i operatora przypisania kopiującego, ponieważ ich domyślna implementacja zostałaby usunięta, gdyby istniały składowe przenoszące.B
(B(const B& other)
) oraz operatora przypisania kopiującego B
(B& operator=(const B& other)
):
A
) jest “przenoszona” za pomocą std::move(other)
. To wywołuje konstruktor przenoszący A
(lub operator przypisania przenoszącego A
) dla pod-obiektu A
wewnątrz other
. Ważne jest, że po tej operacji, stan bazowej części obiektu other
jest niezdefiniowany, ale spójny.m_txt
jest kopiowane tradycyjnie (m_txt(other.m_txt)
lub m_txt = other.m_txt
).To specyficzne połączenie (“przenoszenie” bazowej klasy, która jest move-only
, podczas kopiowania klasy pochodnej) jest rzadkie w praktycznym kodzie, ponieważ zazwyczaj semantyka kopiowania dotyczy całej hierarchii obiektów. Jednakże, ta implementacja spełnia dokładne wymagania zadania.
Ten temat jest szczegółowo omówiony w rozwiązaniu zadania numer 1. Proszę odnieść się do niego, aby uzyskać pełne wyjaśnienie i przykłady.
W skrócie:
std::vector
: Ciągła pamięć, szybki losowy dostęp, wolne wstawianie/usuwanie w środku/na początku.std::list
: Brak ciągłej pamięci (dwukierunkowe), szybkie wstawianie/usuwanie w dowolnym miejscu, tylko iteracyjny dostęp.std::deque
: Kompromis, losowy dostęp, szybkie wstawianie/usuwanie na końcach, ale nie tak szybkie jak lista w środku; brak ciągłej pamięci.#include <cassert>
#include <iostream>
#include <memory> // Dla std::shared_ptr, std::make_shared
#include <string>
#include <utility> // Dla std::move
struct MyResource {
std::string name;
MyResource(const std::string& n) : name(n) {
std::cout << "MyResource ctor: " << name << std::endl;
}
~MyResource() {
std::cout << "MyResource dtor: " << name << std::endl;
}
};
int main() {
// 1. Tworzenie shared_ptr
// Zalecane użycie make_shared, alokuje zasób i strukturę kontrolną w jednym bloku pamięci
std::shared_ptr<MyResource> sptr1 = std::make_shared<MyResource>("Resource A");
// Sprawdzenie, czy sptr1 zarządza zasobem
assert(sptr1); // true, nie jest nullptr
std::cout << "sptr1 use_count: " << sptr1.use_count() << std::endl; // Powinno być 1
// 2. Kopiowanie shared_ptr - zwiększa licznik referencji
{
std::shared_ptr<MyResource> sptr2 = sptr1; // Kopiowanie - sptr2 również zarządza "Resource A"
std::cout << "sptr2 created. sptr1 use_count: " << sptr1.use_count() << std::endl; // Powinno być 2
sptr2->name = "Resource A (modified by sptr2)"; // Dostęp do zasobu
std::cout << "sptr2 name: " << sptr2->name << std::endl;
std::shared_ptr<MyResource> sptr3;
sptr3 = sptr2; // Przypisanie kopiujące - sptr3 również zarządza "Resource A"
std::cout << "sptr3 assigned. sptr1 use_count: " << sptr1.use_count() << std::endl; // Powinno być 3
// Kiedy sptr2 i sptr3 wyjdą z zakresu, liczniki referencji spadną.
// Zasób "Resource A" nie zostanie zniszczony, dopóki sptr1 nadal istnieje.
std::cout << "sptr2 and sptr3 leaving scope." << std::endl;
}
std::cout << "sptr1 use_count after sptr2/sptr3 left scope: " << sptr1.use_count() << std::endl; // Powinno być 1
// 3. Przenoszenie shared_ptr - przenosi własność, nie zwiększa licznika referencji
std::shared_ptr<MyResource> sptr4 = std::make_shared<MyResource>("Resource B");
std::cout << "sptr4 use_count: " << sptr4.use_count() << std::endl; // Powinno być 1
std::shared_ptr<MyResource> sptr5 = std::move(sptr4); // Przeniesienie - sptr5 przejmuje własność
// sptr4 jest teraz puste
assert(!sptr4);
std::cout << "sptr4 moved to sptr5. sptr5 use_count: " << sptr5.use_count() << std::endl; // Powinno być 1
std::cout << "sptr5 name: " << sptr5->name << std::endl;
sptr1.reset(); // sptr1 przestaje zarządzać "Resource A". Licznik referencji spada do 0.
// "Resource A" zostanie zniszczony.
assert(!sptr1);
std::cout << "sptr1 reset. " << std::endl;
// 4. Resetowanie shared_ptr - zwalnia własność i opcjonalnie przejmuje nową
sptr5.reset(new MyResource("Resource C")); // sptr5 przestaje zarządzać "Resource B" (zniszczone),
// a zaczyna zarządzać "Resource C"
std::cout << "sptr5 reset with new resource. sptr5 use_count: " << sptr5.use_count() << std::endl; // Powinno być 1
std::cout << "sptr5 name: " << sptr5->name << std::endl;
// Kiedy sptr5 wyjdzie z zakresu, "Resource C" zostanie zniszczony.
std::cout << "\nEnd of main. Resources will be destroyed." << std::endl;
return 0;
}
Omówienie funkcjonalności std::shared_ptr
:
Shared Ownership
): std::shared_ptr
implementuje semantykę współdzielonej własności. Oznacza to, że wiele obiektów shared_ptr
może jednocześnie zarządzać tym samym dynamicznie zaalokowanym zasobem. Zasób ten jest niszczony, gdy ostatni shared_ptr
do niego zostanie zniszczony lub zresetowany.shared_ptr
do tego samego zasobu) zawiera licznik referencji. Ten licznik jest inkrementowany przy kopiowaniu shared_ptr
i dekrementowany przy jego niszczeniu lub resecie.std::make_shared
): Zaleca się używanie std::make_shared
do tworzenia shared_ptr
. make_shared
optymalizuje alokację, przydzielając pamięć zarówno dla zarządzanego obiektu, jak i dla struktury kontrolnej w jednym bloku, co jest szybsze niż dwie oddzielne alokacje.std::shared_ptr
mogą być kopiowane i przypisywane (sptr2 = sptr1;
). Każda kopia zwiększa licznik referencji, wskazując, że inny właściciel współdzieli zasób.std::move
): Własność może być również przenoszona (sptr5 = std::move(sptr4);
). Operacja przenoszenia transferuje własność z jednego shared_ptr
do drugiego, nie zwiększając licznika referencji. Obiekt źródłowy (np. sptr4
) staje się pusty (nullptr
) po przeniesieniu.reset()
): Funkcja reset()
zwalnia shared_ptr
z zarządzania obecnym zasobem. Jeśli shared_ptr
był ostatnim właścicielem, zasób zostanie zniszczony. Można również przekazać nowy surowy wskaźnik do reset()
, aby shared_ptr
zaczął zarządzać nowym zasobem.->
) i dereferencji (*
). Można również użyć funkcji get()
do uzyskania surowego wskaźnika, ale należy zachować ostrożność, aby nie usunąć danych, które są nadal zarządzane przez shared_ptr
.release()
: W przeciwieństwie do std::unique_ptr
, std::shared_ptr
nie posiada funkcji release()
, która pozwoliłaby na oddanie własności bez niszczenia zasobu. Jest to spowodowane tym, że shared_ptr
ma współdzieloną własność i nie może jednostronnie “odebrać” własności innym shared_ptr
.Kategoria wartości wyrażenia wywołania funkcji (function call expression
) zależy od typu wartości zwracanej przez funkcję.
Wyrażenie foo
, które jest samą nazwą funkcji (bez operatora ()
), jest zawsze l-wartością (lvalue
), ponieważ można pobrać jego adres (np. &foo
).
Jednakże wyrażenie wywołania funkcji (czyli foo(<args>)
):
Jest l-wartością, jeśli funkcja zwraca referencję (dokładniej, referencję l-wartościową).
Przykład:
int& loo() { // Funkcja zwraca referencję do int
static int x = 10;
return x;
}
int main() {
int& l = loo(); // Wywołanie loo() jest l-wartością, więc można zainicjować referencję l-wartościową
int* p = &loo(); // Można pobrać adres z wyrażenia loo(), bo jest l-wartością
}
Jest r-wartością (rvalue
) w przeciwnym wypadku. Czyli, jeśli funkcja zwraca wartość przez wartość (a nie referencję).
Przykład:
int roo() { // Funkcja zwraca int przez wartość
return 0;
}
int main() {
// int& r = roo(); // Błąd: roo() jest r-wartością, nie można wiązać int& do r-wartości
// int* p = &roo(); // Błąd: nie można pobrać adresu z r-wartości
}
W kontekście semantyki przenoszenia, wyrażenie wywołania funkcji, które jest r-wartością (np. zwracające tymczasowy obiekt), jest często źródłem dla operacji przenoszenia, co pozwala na optymalizację wydajności poprzez uniknięcie kopiowania.
To zadanie jest bardzo podobne do zadania 18, ale z odwróconymi wymaganiami dla klas bazowej i pochodnej.
1. Struktura A (tylko do kopiowania): Aby struktura była tylko do kopiowania, musi mieć jawnie zadeklarowane (lub domyślne) składowe kopiujące, a jej składowe przenoszące powinny być usunięte.
#include <string>
#include <utility> // Dla std::move
#include <iostream>
struct A {
std::string m_a; // Pole składowe
A() = default; // Domyślny konstruktor
// Jawnie domyślny konstruktor kopiujący
A(const A& other) = default;
// Jawnie domyślny operator przypisania kopiującego
A& operator=(const A& other) = default;
// Konstruktor przenoszący - usunięty (aby A było tylko do kopiowania)
A(A&&) = delete;
// Operator przypisania przenoszącego - usunięty (aby A było tylko do kopiowania)
A& operator=(A&&) = delete;
// Opcjonalne: konstruktor z argumentem
A(std::string&& val) : m_a(std::move(val)) {
std::cout << "A::ctor from rvalue string: " << m_a << std::endl;
}
A(const std::string& val) : m_a(val) {
std::cout << "A::ctor from lvalue string: " << m_a << std::endl;
}
~A() {
std::cout << "A::dtor: " << m_a << std::endl;
}
};
2. Struktura B (pochodna z A, typ tylko do przenoszenia): Będzie pochodzić od A, mieć pole m_b
typu std::string
. Musi być typem tylko do przenoszenia.
// Struktura B: Pochodna od A. Typ tylko do przenoszenia.
struct B : A {
std::string m_b; // Pole składowe
B() = default; // Domyślny konstruktor
// Konstruktor przenoszący B:
// Wartość obiektu bazowego (A) będzie kopiowana (bo A jest tylko do kopiowania)
// Składowa m_b będzie przenoszona.
B(B&& other) noexcept
: A(other), // Wywołuje A::A(const A& other) - kopiuje bazową A (bo A jest copy-only)
m_b(std::move(other.m_b)) // Wywołuje std::string::string(std::string&&) - przenosi m_b
{
std::cout << "B::move-ctor: (A-base copied, m_b moved)" << std::endl;
}
// Operator przypisania przenoszącego B:
// Wartość obiektu bazowego (A) będzie kopiowana
// Składowa m_b będzie przenoszona.
B& operator=(B&& other) noexcept {
std::cout << "B::move-assign: (A-base copied, m_b moved)" << std::endl;
if (this != &other) {
A::operator=(other); // Wywołuje A::operator=(const A&) - kopiuje bazową A
m_b = std::move(other.m_b); // Wywołuje std::string::operator=(std::string&&) - przenosi m_b
}
return *this;
}
// Aby B było typem TYLKO DO PRZENOSZENIA,
// konstruktor kopiujący i operator przypisania kopiującego muszą być usunięte.
B(const B&) = delete;
B& operator=(const B&) = delete;
// Opcjonalne: konstruktor z argumentami
B(std::string_view a_val, std::string_view b_val)
: A(std::string(a_val)), m_b(b_val) {
std::cout << "B::ctor from strings: " << m_a << ", " << m_b << std::endl;
}
~B() {
std::cout << "B::dtor: " << m_a << ", " << m_b << std::endl;
}
};
int main() {
std::cout << "--- Test A (copy-only) ---" << std::endl;
A a1("Initial A");
A a2 = a1; // OK: copy-ctor
// A a3 = std::move(a1); // ERROR: move-ctor deleted
std::cout << "\n--- Test B (move-only, A-base copied, m_b moved) ---" << std::endl;
B b1("BaseName1", "DataTxt1");
std::cout << "b1: (A::m_a = " << b1.m_a << ", B::m_b = " << b1.m_b << ")" << std::endl;
B b2 = std::move(b1); // OK: B::move-ctor
std::cout << "b2 (move of b1): (A::m_a = " << b2.m_a << ", B::m_b = " << b2.m_b << ")" << std::endl;
std::cout << "b1 after move to b2 (m_b moved): (A::m_a = " << b1.m_a << ", B::m_b = " << b1.m_b << ")" << std::endl;
B b3("BaseName3", "DataTxt3");
std::cout << "b3 before assign: (A::m_a = " << b3.m_a << ", B::m_b = " << b3.m_b << ")" << std::endl;
b3 = std::move(b2); // OK: B::move-assign
std::cout << "b3 after assign from b2: (A::m_a = " << b3.m_a << ", B::m_b = " << b3.m_b << ")" << std::endl;
std::cout << "b2 after assign to b3 (m_b moved): (A::m_a = " << b2.m_a << ", B::m_b = " << b2.m_b << ")" << std::endl;
// B b4 = b1; // ERROR: B::copy-ctor deleted
std::cout << "\n--- End of main ---" << std::endl;
return 0;
}
Omówienie:
A
: Jest skonfigurowana jako typ tylko do kopiowania. Jej konstruktor przenoszący i operator przypisania przenoszącego są jawnie usunięte (= delete
), co wymusza użycie operacji kopiowania.B
:
A
.B
(B(B&& other)
) oraz operatora przypisania przenoszącego B
(B& operator=(B&& other)
):
A
) jest kopiowana (A(other)
lub A::operator=(other)
), ponieważ A
jest typem tylko do kopiowania.m_b
jest przenoszone (m_b(std::move(other.m_b))
lub m_b = std::move(other.m_b)
), ponieważ B
jest typem przenoszącym, a std::string
wspiera semantykę przenoszenia. To spełnia precyzyjne wymagania zadania dotyczące tego, które części obiektu są kopiowane, a które przenoszone, w zależności od hierarchii i specyfikacji typu.std::weak_ptr
służy do śledzenia zarządzanych danych bez przejmowania własności. Oznacza to, że weak_ptr
nie zwiększa licznika referencji obiektu std::shared_ptr
i nie utrzymuje obiektu przy życiu.
Problemy z bezpośrednim dostępem: std::weak_ptr
celowo nie oferuje bezpośredniego sposobu na uzyskanie surowego wskaźnika do zarządzanych danych za pomocą operatorów dereferencji (operator*
), dostępu do składowej przez wskaźnik (operator->
) czy funkcji get()
. Argumentacja jest następująca:
dangling pointer
): Nawet jeśli w momencie sprawdzenia weak_ptr
obiekt zarządzany istnieje, nic nie gwarantuje, że nie zostanie zniszczony chwilę później (np. przez inny wątek lub gdy ostatni shared_ptr
wyjdzie z zakresu), zanim zdążymy go użyć. Bezpośrednie uzyskanie surowego wskaźnika mogłoby szybko stać się wiszącym wskaźnikiem, prowadząc do niezdefiniowanego zachowania.Bezpieczne uzyskiwanie dostępu (blokowanie własności): Jedynym bezpiecznym sposobem dostępu do danych zarządzanych przez weak_ptr
jest “zablokowanie” własności poprzez utworzenie z niego obiektu std::shared_ptr
. Można to zrobić na dwa sposoby:
Wywołanie konstruktora std::shared_ptr
z weak_ptr
:
std::shared_ptr<T> sptr(wptr);
weak_ptr
wygasł (obiekt już nie istnieje), ten konstruktor rzuca wyjątek std::bad_weak_ptr
.shared_ptr
i zwiększa licznik referencji, gwarantując, że obiekt będzie istniał, dopóki ten nowy shared_ptr
jest aktywny.Wywołanie funkcji lock()
na weak_ptr
:
std::shared_ptr<T> sptr = wptr.lock();
weak_ptr
wygasł, lock()
zwróci pusty std::shared_ptr
(tj. nullptr
).lock()
zwróci prawidłowy shared_ptr
, również zwiększając licznik referencji.Przykład:
#include <iostream>
#include <memory> // Dla std::shared_ptr, std::weak_ptr
struct MyData {
int value;
MyData(int v) : value(v) { std::cout << "MyData(" << value << ") ctor" << std::endl; }
~MyData() { std::cout << "MyData(" << value << ") dtor" << std::endl; }
};
int main() {
std::shared_ptr<MyData> sptr_owner;
std::weak_ptr<MyData> wptr_tracker;
{
// Tworzymy shared_ptr, który jest właścicielem danych
sptr_owner = std::make_shared<MyData>(123);
wptr_tracker = sptr_owner; // weak_ptr zaczyna śledzić dane
std::cout << "\n--- Obiekt istnieje (sptr_owner w zakresie) ---" << std::endl;
// Sprawdzanie, czy weak_ptr nie wygasł
if (!wptr_tracker.expired()) {
std::cout << "Weak pointer is not expired." << std::endl;
// Bezpieczny dostęp poprzez lock()
std::shared_ptr<MyData> temp_sptr = wptr_tracker.lock();
if (temp_sptr) { // Sprawdź, czy lock() zwrócił prawidłowy shared_ptr
std::cout << "Accessed data: " << temp_sptr->value << std::endl;
std::cout << "Temporary shared_ptr use_count: " << temp_sptr.use_count() << std::endl;
}
}
std::cout << "sptr_owner use_count: " << sptr_owner.use_count() << std::endl;
} // sptr_owner wychodzi z zakresu, MyData(123) zostanie zniszczone
std::cout << "\n--- Obiekt został zniszczony (sptr_owner poza zakresem) ---" << std::endl;
// Po wyjściu sptr_owner z zakresu, obiekt MyData został zniszczony.
// weak_ptr jest teraz 'wygasły'.
if (wptr_tracker.expired()) {
std::cout << "Weak pointer is now expired. Object is gone." << std::endl;
// Próba dostępu poprzez lock() - zwróci nullptr
std::shared_ptr<MyData> temp_sptr = wptr_tracker.lock();
if (!temp_sptr) {
std::cout << "Lock returned a nullptr shared_ptr." << std::endl;
}
// Próba dostępu poprzez konstruktor shared_ptr - rzuci wyjątek
try {
std::shared_ptr<MyData> sptr_exception(wptr_tracker);
} catch (const std::bad_weak_ptr& e) {
std::cout << "Caught exception: " << e.what() << std::endl;
}
}
std::cout << "\nEnd of main." << std::endl;
return 0;
}
#include <vector>
#include <string>
#include <algorithm> // Dla std::sort
#include <iostream>
#include <utility> // Dla std::pair
int main() {
std::vector<std::pair<std::string, int>> data = {
{"Anna", 30},
{"Jan", 25},
{"Zofia", 30},
{"Piotr", 20},
{"Marek", 25}
};
std::cout << "Original data:" << std::endl;
for (const auto& p : data) {
std::cout << p.first << ", " << p.second << std::endl;
}
// Sortowanie za pomocą std::sort i wyrażenia lambda
// Kryteria:
// 1. Najpierw po wartości int (rosnąco)
// 2. Jeśli wartości int są równe, to po wartości string (rosnąco)
std::sort(data.begin(), data.end(), [](const std::pair<std::string, int>& a,
const std::pair<std::string, int>& b) {
// Porównaj najpierw po int (drugi element pair)
if (a.second != b.second) {
return a.second < b.second; // Rosnąco po int
}
// Jeśli inty są równe, porównaj po string (pierwszy element pair)
return a.first < b.first; // Rosnąco po string
});
std::cout << "\nSorted data (by int then by string):" << std::endl;
for (const auto& p : data) {
std::cout << p.first << ", " << p.second << std::endl;
}
return 0;
}
Omówienie: Funkcja std::sort
umożliwia niestandardowe sortowanie poprzez przekazanie funkcji porównującej (tzw. callable
) jako trzeciego argumentu. W tym przykładzie używamy wyrażenia lambda [](...) { ... }
do zdefiniowania logiki porównywania.
Lambda przyjmuje dwa obiekty std::pair<std::string, int>
(a
i b
) i zwraca true
, jeśli a
powinno być umieszczone przed b
w posortowanym zbiorze.
int
(dostępne jako a.second
i b.second
) są różne. Jeśli tak, sortujemy według nich rosnąco (a.second < b.second
).int
są takie same, przechodzimy do porównania po wartościach std::string
(dostępnych jako a.first
i b.first
), sortując je również rosnąco (a.first < b.first
).Ta lambda efektywnie implementuje dwupoziomowe sortowanie, najpierw według jednego kryterium, a następnie, w przypadku równości, według drugiego.
Iteratory są uogólnieniem wskaźników i stanowią “klej” między kontenerami a algorytmami w C++. Służą do abstrakcyjnego dostępu do elementów kontenera, niezależnie od jego konkretnego typu. Można powiedzieć, że wskaźnik do tablicy w stylu C jest iteratorem.
Kluczowe cechy i funkcjonalności iteratorów:
*i
) oraz inkrementowane (++i
). Niektóre typy iteratorów (np. random-access) wspierają także operacje arytmetyczne (i + N
, i - N
).T
zdefiniowane są co najmniej dwa typy iteratorów:
T::iterator
: Iterator niemodyfikowalny (non-const
), który pozwala na modyfikowanie elementów kontenera.T::const_iterator
: Iterator const
, który pozwala tylko na odczyt elementów kontenera, ale nie na ich modyfikowanie. Zaleca się używanie const_iterator
, jeśli nie modyfikujesz elementów.begin()
i end()
:
begin()
zwraca iterator do pierwszego elementu kontenera.end()
zwraca iterator za ostatni element (past-the-end iterator
). Jest to iterator do “wyimaginowanego” elementu, który następuje po ostatnim. Jeśli kontener jest pusty, begin()
i end()
zwracają równe iteratory.cbegin()
i cend()
(dostępne od C++11) zawsze zwracają const_iterator
, nawet dla niemodyfikowalnych kontenerów. Są one udogodnieniem, ale ich funkcjonalność można osiągnąć poprzez użycie std::as_const
z begin()
/end()
.Forward iterator
): Obsługuje tylko dwie podstawowe operacje: dereferencję (*i
) i inkrementację (++i
). Przykład: iterator std::forward_list
.Bidirectional iterator
): Jest iteratorem do przodu z dodatkową operacją dekrementacji (--i
). Przykład: iterator std::list
.Random-access iterator
): Pozwala na zwiększanie lub zmniejszanie wartości iteratora o dowolną liczbę całkowitą (np. i + N
, i - N
, i += N
). Przykład: iteratory std::vector
i std::deque
. Surowy wskaźnik jest również iteratorem z dostępem swobodnym.old way
): Tradycyjna pętla for
z ręczną inicjalizacją, warunkiem i inkrementacją iteratora (np. for (auto i = v.begin(); i != v.end(); ++i)
).new way
): Pętla for-each
(range-based for loop, od C++11). Jest mniej podatna na błędy i bardziej zwięzła (for (declaration : expression) statement
). Kompilator tłumaczy ją na tradycyjną pętlę wykorzystującą begin()
, end()
, !=
, *
i ++
.W C++ elementy kontenera, zwłaszcza te, które mają być sortowane (np. w std::sort
, std::priority_queue
, std::set
, std::multiset
), mogą być porównywane na kilka sposobów, co zapewnia dużą elastyczność w ustalaniu porządku. Algorytmy standardowe, takie jak std::sort
, nie używają bezpośrednio operatora <
do porównania, lecz przyjmują obiekt callable do niestandardowego porównywania.
Główne sposoby porównywania elementów to:
Domyślne użycie operatora <
(less-than operator):
std::sort
, std::priority_queue
, std::set
, std::map
) domyślnie używa operatora <
(less-than operator) do ustalenia porządku między elementami.int
), ten operator jest predefiniowany.user-defined types
), należy przeciążyć operator <
jako funkcję składową (member function
) lub niezależną funkcję. Operator powinien być zadeklarowany jako const
i przyjmować drugi operand przez const
referencję.Użycie standardowych funktorów (std::less
, std::greater
):
C++
dostarcza funktorów w nagłówku <functional>
, takich jak std::less<T>
i std::greater<T>
.std::less<T>
: Jest domyślnie używany przez std::sort
i std::priority_queue
(domyślnie zwracającą największy element). Zachowuje się jak operator <
.std::greater<T>
: Umożliwia sortowanie w kolejności malejącej (lub sprawienie, by priority_queue
zwracała najmniejszy element). Wymaga, aby typ T
miał zdefiniowany operator >
.inlined
).Wskaźniki na funkcje (Function pointers
):
C
) używano wskaźników na funkcje do przekazywania logiki porównywania. W C++ nadal jest to możliwe.bool
, a następnie przekazać wskaźnik do tej funkcji (lub samą nazwę, która “rozpada się” na wskaźnik) do algorytmu.Funktory (klasy z przeciążonym operator()
):
()
. Jest to klasa lub struktura, która przeciąża ten operator.std::less
i std::greater
są przykładami funktorów. Można również zdefiniować własną strukturę z operator()
do niestandardowej logiki porównywania.Wyrażenia lambda (Lambda expressions
):
syntactic sugar
) dla funktorów. Umożliwiają wygodne tworzenie obiektów callable (domknięć) bez konieczności definiowania pełnej klasy funktora.std::vector
malejąco lub do bardziej złożonego porównywania z flagami, jak w zadaniu 2.Wybór metody zależy od złożoności logiki porównywania i tego, czy potrzebny jest stan (dodatkowe dane) do przeprowadzenia porównania. Funktory i lambdy są zazwyczaj preferowane ze względu na ich elastyczność i możliwość przechwytywania stanu.
#include <iostream>
#include <string>
#include <vector>
#include <list>
#include <utility> // Dla std::move
int main() {
// 1. Utwórz vector<string> i umieść w nim element "Hello World"
std::vector<std::string> my_vector;
my_vector.push_back("Hello World"); // Kopiuje lub przenosi tymczasowy string do vectora
std::cout << "Vector contents: ";
for (const auto& s : my_vector) {
std::cout << "\"" << s << "\" ";
}
std::cout << std::endl;
// 2. Utwórz list<string>
std::list<std::string> my_list;
my_list.push_back("Existing Element"); // Dodajmy coś, żeby początek nie był pusty
std::cout << "List contents before move: ";
for (const auto& s : my_list) {
std::cout << "\"" << s << "\" ";
}
std::cout << std::endl;
// 3. Przenieś napis z vectora na początek listy
// Używamy std::move, aby przenieść wartość z elementu vectora
// std::list::emplace_front jest preferowane, ponieważ konstruuje element w miejscu
if (!my_vector.empty()) {
// my_vector to l-wartość, więc używamy std::move, aby uczynić ją r-wartością
my_list.emplace_front(std::move(my_vector));
}
std::cout << "Vector contents after move (original element moved out): ";
for (const auto& s : my_vector) {
// Po przeniesieniu, stan my_vector jest niezdefiniowany, ale spójny (często pusty)
std::cout << "\"" << s << "\" ";
}
std::cout << std::endl;
std::cout << "List contents after move: ";
for (const auto& s : my_list) {
std::cout << "\"" << s << "\" ";
}
std::cout << std::endl;
return 0;
}
Omówienie:
std::vector<std::string>
i dodajemy do niego std::string("Hello World")
. Użycie push_back
z tymczasowym obiektem std::string
spowoduje, że zostanie on przeniesiony do wektora (jeśli std::string
wspiera semantykę przenoszenia), lub skopiowany.std::list<std::string>
.my_vector
. Jest to l-wartość.std::move(my_vector)
, aby jawnie skonwertować tę l-wartość na r-wartość. Jest to kluczowe, aby aktywować semantykę przenoszenia dla obiektu std::string
.my_list.emplace_front(...)
. Funkcje emplace
są preferowane, ponieważ konstruują obiekt w miejscu (in-place
) w kontenerze, unikając niepotrzebnego kopiowania lub przenoszenia. Dzięki std::move
funkcja emplace_front
będzie w stanie wykorzystać konstruktor przenoszący std::string
, aby efektywnie przenieść dane napisu z my_vector
do nowego elementu na początku my_list
.std::move
), stan elementu my_vector
jest niezdefiniowany, ale spójny. W przypadku std::string
zazwyczaj oznacza to, że napis staje się pusty.Zarówno std::unique_ptr
, jak i std::shared_ptr
to inteligentne wskaźniki (smart pointers
) zdefiniowane w nagłówku <memory>
, które rozwiązują typowe problemy z surowymi wskaźnikami (raw pointers
).
Podobieństwa:
managed data
).type problem
), problem własności (ownership problem
) i problem obsługi wyjątków (exception handling problem
), automatyzując dealokację pamięci.exception-safe
): Oba są bezpieczne w kontekście wyjątków, gwarantując zwolnienie zasobów nawet w przypadku ich wystąpienia.Różnice:
Cecha
std::unique_ptr
std::shared_ptr
Semantyka własności
Wyłączna własność (Exclusive Ownership
). Jest to jedyny właściciel zarządzanych danych.
Współdzielona własność (Shared Ownership
). Wiele shared_ptr
może jednocześnie zarządzać tym samym zasobem.
Kopiowanie
Nie można kopiować (move-only type
). Konstruktor kopiujący i operator przypisania kopiującego są jawnie usunięte.
Można kopiować. Kopiowanie zwiększa licznik referencji w strukturze kontrolnej.
Przenoszenie
Można przenosić własność (std::move
). Po przeniesieniu źródłowy unique_ptr
staje się pusty.
Można przenosić własność (std::move
). Po przeniesieniu źródłowy shared_ptr
staje się pusty.
Rozmiar w pamięci
Zazwyczaj rozmiar surowego wskaźnika.
Dwa razy większy niż surowy wskaźnik, ponieważ przechowuje wskaźnik do danych i wskaźnik do struktury kontrolnej.
Struktura kontrolna
Nie posiada struktury kontrolnej [brak wzmianki, ale logiczne, że nie potrzebuje].
Posiada i współdzieli strukturę kontrolną, która zawiera licznik referencji i licznik słabych referencji.
Zniszczenie danych
Zarządzane dane są niszczone, gdy unique_ptr
zostanie zniszczony (wyjdzie z zakresu) lub zresetowany.
Zarządzane dane są niszczone, gdy ostatni shared_ptr
do nich zostanie zniszczony.
Funkcja release()
Posiada funkcję release()
, która zwalnia własność, ale nie niszczy zarządzanych danych.
Nie posiada funkcji release()
, ponieważ nie może jednostronnie zwolnić zasobu od innych współwłaścicieli.
Konwersja
Może być przeniesiony do shared_ptr
(konwersja jednokierunkowa).
Nie może być przeniesiony do unique_ptr
(z powodu semantyki współdzielonej własności).
Sposób tworzenia
Często używa std::make_unique
.
Często używa std::make_shared
.
std::unique_ptr
jest zazwyczaj preferowany, gdy wystarczy wyłączna własność, ponieważ jest lżejszy i bardziej wydajny. std::shared_ptr
jest niezbędny, gdy wiele części programu musi współdzielić ten sam zasób i automatycznie zarządzać jego cyklem życia.
To zadanie jest niemal identyczne z zadaniem numer 4, z tą różnicą, że explicitly prosi o wzorzec “singleton” i podkreśla, że funkcja ma “śledzić, ale nie posiadać na własność”.
#include <iostream>
#include <memory> // Dla std::shared_ptr, std::weak_ptr
// Klasa A, którą będzie zarządzać singleton
struct A {
A() {
std::cout << "ctor: Singleton A created." << std::endl;
}
~A() {
std::cout << "dtor: Singleton A destroyed." << std::endl;
}
void greet() const {
std::cout << "Hello from Singleton A!" << std::endl;
}
};
// Funkcja singleton zwracająca shared_ptr do obiektu klasy A
std::shared_ptr<A> get_singleton_A() {
// Statyczny weak_ptr do śledzenia (ale nie posiadania) instancji A.
// Jest statyczny, aby jego stan był zachowany między wywołaniami funkcji.
static std::weak_ptr<A> singleton_instance_tracker;
// Próbujemy uzyskać shared_ptr z weak_ptr.
// lock() zwróci shared_ptr, jeśli obiekt nadal istnieje, w przeciwnym razie nullptr.
std::shared_ptr<A> current_instance = singleton_instance_tracker.lock();
if (!current_instance) { // Jeśli obiekt nie istnieje (lub wygasł)
std::cout << "Singleton A does not exist or expired. Creating a new instance." << std::endl;
// Tworzymy nową instancję A za pomocą make_shared (zalecane)
current_instance = std::make_shared<A>();
// Aktualizujemy weak_ptr, aby śledził tę nową instancję.
// weak_ptr nie zwiększa licznika referencji shared_ptr.
singleton_instance_tracker = current_instance;
} else {
std::cout << "Singleton A still exists. Reusing existing instance." << std::endl;
}
return current_instance;
}
int main() {
{
std::cout << "--- First request for Singleton A ---" << std::endl;
std::shared_ptr<A> s1 = get_singleton_A(); // Utworzy nową instancję A
s1->greet();
std::cout << "s1 use_count: " << s1.use_count() << std::endl; // Powinno być 1
std::cout << "\n--- Second request for Singleton A (object still alive) ---" << std::endl;
std::shared_ptr<A> s2 = get_singleton_A(); // Użyje istniejącej instancji
s2->greet();
std::cout << "s1 use_count: " << s1.use_count() << std::endl; // Powinno być 2
std::cout << "s2 use_count: " << s2.use_count() << std::endl; // Powinno być 2
// Kiedy s1 i s2 wyjdą z zakresu, licznik referencji shared_ptr spadnie do zera,
// a obiekt A zostanie zniszczony.
std::cout << "\nLeaving inner scope (s1 and s2 will be destroyed)." << std::endl;
} // Tutaj obiekt A zostanie zniszczony, jeśli s1 i s2 były jedynymi shared_ptr
std::cout << "\n--- Third request for Singleton A (object likely destroyed) ---" << std::endl;
std::shared_ptr<A> s3 = get_singleton_A(); // Utworzy nową instancję, jeśli poprzednia została zniszczona
s3->greet();
std::cout << "s3 use_count: " << s3.use_count() << std::endl; // Powinno być 1
std::cout << "\nEnd of main." << std::endl;
return 0;
}
Omówienie: Implementacja funkcji get_singleton_A
wykorzystuje static std::weak_ptr<A> singleton_instance_tracker;
. std::weak_ptr
jest idealne do tego zadania, ponieważ pozwala funkcji śledzić obiekt bez posiadania go na własność. Oznacza to, że weak_ptr
nie wpływa na licznik referencji shared_ptr
i nie utrzymuje obiektu przy życiu. Czas życia obiektu jest w pełni zarządzany przez shared_ptr
y trzymane przez klientów funkcji.
Logika funkcji:
lock()
) weak_ptr
singleton_instance_tracker
.lock()
zwróci niepusty std::shared_ptr
, oznacza to, że instancja A
nadal istnieje i jest zarządzana przez co najmniej jeden shared_ptr
. Funkcja zwraca wtedy ten istniejący shared_ptr
.lock()
zwróci pusty std::shared_ptr
(czyli weak_ptr
wygasł, bo licznik shared_ptr
spadł do zera i obiekt został zniszczony), funkcja tworzy nową instancję A
za pomocą std::make_shared<A>()
. Następnie singleton_instance_tracker
jest aktualizowany, aby śledził tę nową instancję, zanim zostanie ona zwrócona.W ten sposób funkcja get_singleton_A
działa jak “leniwym” singleton, tworząc obiekt tylko wtedy, gdy jest on potrzebny i nie istnieje, a jednocześnie pozwalając na jego automatyczne zniszczenie, gdy nikt już go nie używa.