Zgodnie z Twoim zapytaniem, poniżej przedstawiam rozwiązania wszystkich zadań z pliku Zadania-1.pdf, bazując na dostarczonych źródłach.


1. Podobieństwo i różnice: std::vector, std::deque, std::list

Podobieństwa:

Różnice:

2. Struktura student z polami m_firstname i m_surname typu string, zaimplementuj lambde, która porównuje obiekty student najpierw po nazwisku potem po imieniu. Lambda ma posiadać flagę do odwracania kolejności.

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

3. Problemy raw pointers.

Używanie “surowych” wskaźników (raw pointers) w C++ jest bardzo podatne na błędy. Główne problemy to:

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.

4. Proszę zaimplementować funkcję factory, która zwraca shared_ptr do obiektu klasy A. Podczas pierwszego wywołania funkcja powinna utworzyć obiekt domyślnie, a podczas kolejnych wywołań zwracać ten sam obiekt, jeśli nadal istnieje. W przeciwnym razie powinna utworzyć nowy obiekt.

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:

  1. Najpierw próbuje uzyskać std::shared_ptr z last_created_A za pomocą funkcji lock().
  2. Jeśli 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.
  3. Jeśli 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.

5. Konwencja wywołania funkcji.

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:

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.

6. Proszę napisać trzy przeciążenia funkcji foo dla typów referencyjnych, a następnie wywołać funkcję z argumentem kategorii l-wartość bądź r-wartość, wraz z omówieniem, które przeciążenie będzie wybrane w zależności od dostępności przeciążeń.

#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:

Kluczowe punkty:

7. Konwersja z l-wartości do r-wartości: jawna a niejawna.

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):

2. Jawna konwersja (Explicit conversion):

8. Proszę zaimplementować strukturę A jako typ tylko do przenoszenia, która ma jedno pole składowe string m_name. Następnie proszę zaimplementować strukturę B wyprowadzoną z A, której składowe kopiujące powinny przenosić wartość obiektu bazowego.

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:

9. Miejsca pamięci: sterta a stos.

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):

2. Sterta (Heap):

10. Proszę napisać kod, który jawnie konwertuje l-wartość na r-wartość i z jej użyciem wywołuje funkcję, która przyjmuje tylko r-wartość.

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

11. Wyrażenie lambda.

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:

12. Proszę napisać kod, który tworzy dynamicznie dwa ciągi znaków „Hello” i „World”, a następnie umieszcza w wektorze do nich obiekty shared_ptr, a w liście obiekty weak_ptr do nich.

#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:

13. Umieszczanie a wstawianie w kontenerach.

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

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.

14. Proszę stworzyć wektor liczb całkowitych, a następnie posortować go malejąco z użyciem funkcji std::sort i wyrażenia lambda.

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

15. Struktura sterująca w shared_ptr i weak_ptr.

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:

Pola w strukturze kontrolnej:

  1. Licznik referencji (Reference Count):

    • Jest to pole w strukturze kontrolnej, które przechowuje liczbę aktywnych obiektów std::shared_ptr wskazujących na zarządzane dane.
    • Gdy obiekt std::shared_ptr jest kopiowany, licznik referencji jest inkrementowany.
    • Gdy obiekt std::shared_ptr jest niszczony (np. wychodzi z zakresu), licznik referencji jest dekrementowany.
    • Zniszczenie zarządzanych danych: Zarządzane dane są niszczone w momencie, gdy licznik referencji osiągnie zero. Oznacza to, że nie ma już żadnych obiektów std::shared_ptr posiadających własność tych danych.
  2. Licznik słabych referencji (Weak Count):

    • Jest to również pole w strukturze kontrolnej, które przechowuje liczbę aktywnych obiektów std::weak_ptr śledzących zarządzane dane.
    • Obiekty std::weak_ptr nie zwiększają licznika referencji, co oznacza, że nie posiadają własności obiektu i nie utrzymują go przy życiu.
    • Zniszczenie struktury kontrolnej: Struktura kontrolna danych jest niszczona dopiero wtedy, gdy zarówno licznik referencji, jak i licznik słabych referencji osiągną zero. Jest to ważne, ponieważ 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ść:

16. Proszę zaimplementować my_unique_ptr o okrojonej funkcjonalności std::unique_ptr: konstruktor, składowe specjalne i funkcja get.

#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:

17. Proszę omówić przeciążenie funkcji w zależności od kategorii wartości argumentu wywołania funkcji.

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:

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

18. struct A { A() = default; A(const A &) = delete; }; Zmienić strukturę A, żeby była typem tylko do przenoszenia. * Zaimplementować strukturę B, która będzie: * Wyprowadzona z A i miała pole składowe m_txt typu std::string. * Typem tylko do kopiowania (bez przenoszenia), gdzie wartość obiektu bazowego będzie przenoszona, a składowego kopiowana.

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:

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.

19. Kontenery std::vector, std::deque, std::list: podobieństwa i różnice.

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:

20. Proszę napisać fragmenty kodu (wraz z omówieniem), które prezentują funkcjonalność std::shared_ptr.

#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:

21. Kategoria wartości wyrażenia wywołania funkcji.

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>)):

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.

22. Napisać strukturę A z polem m_a typu std::string. Struktura A ma być tylko do kopiowania. Wyprowadzić strukturę B z A. B jest typem tylko do przenoszenia i ma jedno pole m_b typu std::string.

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:

23. Dostęp do danej zarządzanej przez weak_ptr: przykład, argumentacja.

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:

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:

  1. Wywołanie konstruktora std::shared_ptr z weak_ptr:

    • std::shared_ptr<T> sptr(wptr);
    • Jeśli weak_ptr wygasł (obiekt już nie istnieje), ten konstruktor rzuca wyjątek std::bad_weak_ptr.
    • Jeśli obiekt istnieje, tworzy nowy shared_ptr i zwiększa licznik referencji, gwarantując, że obiekt będzie istniał, dopóki ten nowy shared_ptr jest aktywny.
  2. Wywołanie funkcji lock() na weak_ptr:

    • std::shared_ptr<T> sptr = wptr.lock();
    • Jest to preferowany sposób, ponieważ jest bezpieczny od wyjątków. Jeśli weak_ptr wygasł, lock() zwróci pusty std::shared_ptr (tj. nullptr).
    • Jeśli obiekt istnieje, 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;
}

24. Posortuj vector<pair<string,int>> najpierw po int, później po string, przy użyciu std::sort() i wyrażenia lambda.

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

  1. Najpierw sprawdzamy, czy wartości 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).
  2. Jeśli wartości 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.

25. Iteratory.

Iteratoryuogó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:

26. Proszę opisać sposoby porównywania elementów kontenera w celu ich sortowania.

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:

  1. Domyślne użycie operatora < (less-than operator):

    • Wiele algorytmów i kontenerów standardowych (np. std::sort, std::priority_queue, std::set, std::map) domyślnie używa operatora < (less-than operator) do ustalenia porządku między elementami.
    • Dla wbudowanych typów (np. int), ten operator jest predefiniowany.
    • Dla typów zdefiniowanych przez użytkownika (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ę.
  2. Użycie standardowych funktorów (std::less, std::greater):

    • Biblioteka standardowa 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 >.
    • Te funktory nie przechowują żadnych danych, więc nie wpływają na wydajność; ich konstruktory i destruktory są puste, a funkcja porównująca jest wbudowana (inlined).
  3. Wskaźniki na funkcje (Function pointers):

    • W C (C) używano wskaźników na funkcje do przekazywania logiki porównywania. W C++ nadal jest to możliwe.
    • Można zdefiniować zwykłą funkcję, która przyjmuje dwa elementy do porównania i zwraca bool, a następnie przekazać wskaźnik do tej funkcji (lub samą nazwę, która “rozpada się” na wskaźnik) do algorytmu.
    • Ograniczenie: Wskaźnik na funkcję nie może przechowywać dodatkowych danych (stanu), które mogłyby być użyte w porównaniu.
  4. Funktory (klasy z przeciążonym operator()):

    • Funktor to obiekt, na którym można wywołać operator (). Jest to klasa lub struktura, która przeciąża ten operator.
    • Zaleta: Funktory są bardziej wszechstronne niż wskaźniki na funkcje, ponieważ mogą przechowywać dodatkowe dane przekazane do ich konstruktora. Te dane mogą być następnie używane w logice porównywania.
    • Przykład: Klasy 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.
  5. Wyrażenia lambda (Lambda expressions):

    • Lambda jest syntaktycznym cukrem (syntactic sugar) dla funktorów. Umożliwiają wygodne tworzenie obiektów callable (domknięć) bez konieczności definiowania pełnej klasy funktora.
    • Zalety:
      • Są zwięzłe i łatwe do napisania “w miejscu”.
      • Mogą przechwytywać zmienne z otaczającego zakresu (przez wartość lub referencję), co pozwala im przechowywać stan i dostosowywać logikę porównywania w czasie działania programu, podobnie jak funktory.
    • Przykład: Można użyć lambdy do sortowania 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.

27. W vector umieścić element “Hello World”, a następnie przenieść ten napis na początek list.

#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:

  1. Tworzymy 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.
  2. Tworzymy std::list<std::string>.
  3. Aby przenieść napis z wektora na początek listy:
    • Uzyskujemy dostęp do elementu w wektorze za pomocą my_vector. Jest to l-wartość.
    • Używamy std::move(my_vector), aby jawnie skonwertować tę l-wartość na r-wartość. Jest to kluczowe, aby aktywować semantykę przenoszenia dla obiektu std::string.
    • Wywołujemy 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.
    • Po przeniesieniu (std::move), stan elementu my_vector jest niezdefiniowany, ale spójny. W przypadku std::string zazwyczaj oznacza to, że napis staje się pusty.

28. Podobieństwa i różnice między unique_ptr a shared_ptr.

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:

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.

29. Proszę zaimplementować funkcję singleton, która zwraca shared_ptr do domyślnie skonstruowanego obiektu klasy A. Funkcja powinna śledzić, ale nie posiadać na własność utworzonego obiektu. Przy ponownym wywołaniu funkcja powinna zwracać shared_ptr do tego samego obiektu, jeśli nadal istnieje, w przeciwnym razie powinna utworzyć nowy obiekt.

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:

  1. Przy każdym wywołaniu, funkcja próbuje “zablokować” (lock()) weak_ptr singleton_instance_tracker.
  2. Jeśli 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.
  3. Jeśli 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.