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_ptry 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.