Klasy - zarządzanie zasobami

Konstruktor klasy jest odpowiedzialny za konstrukcję obiektu w prawidłowym stanie. Czasami aby utworzyć poprawnie działający obiekt wymagane jest pozyskanie zasobów (np. pamięć ze sterty, otwarcie pliku, otwarcie socketu, itp.).

class Vector
{
private:
    size_t size_;
    double* items_;
public:
    Vector(size_t size, double value);
};

Vector::Vector(size_t size, double value) : size_{size}, items_{ new double[size_] }
{}

Destruktor klasy

Jeśli obiekt klasy pozyskuje zasób, to po wykorzystaniu go zasób ten powinien zostać zwolniony. W rezultacie niektóre klasy potrzebują funkcji, która będzie wywołana w momencie niszczenia obiektu i będzie odpowiedzialna za zwolnienie wykorzystywanych zasobów. Taką rolę pełni destruktor klasy.

Destruktory są dla obiektów niejawnie w momencie gdy obiekt ma zostać zniszczony:

  • dla zmiennych automatycznych gdy opuszczany jest zakres, w którym zostały zadeklarowane

  • dla zmiennych dynamicznych, gdy zwalniana jest pamięć za pomocą operatorów delete.

Destruktor może zostać również wywołany jawnie.

Nazwa destruktora – nazwa klasy poprzedzona tyldą: np. ~Vector().

class Vector
{
private:
    size_t size_;
    double* items_;
public:
    Vector(size_t size, double value);
    ~Vector();
};

Vector::Vector(size_t size, double value) : size_{size}, items_{ new double[size_] }
{}

Vector::~Vector()
{
    delete[] items_;
}

Technika, która wiąże się z pozyskaniem zasobu w konstruktorze i zwolnieniem go w destruktorze jest nazywana RAII - Resource Acquisition Is Initialization.

Kopiowanie

Obiekty są domyślnie kopiowalne. Domyślna operacja kopiowania jest definiowana przez kompilator i jest wartościowo realizowana pole po polu.

class BankAccount
{
public:
    BankAccount(double balance, long number, const std::string& owner);
private:
    double balance_;
    long number_;
    string owner_;
};

BankAccount::BankAccount(double balance, long number, const std::string& owner)
    : balance{balance}, number_{n}, owner_{o}
{
}

// ...

int main()
{
    BankAccount account1{1, 1000.0, "Jan Kowalski"};

    BankAccount copy_of_acc = account1;  // kopiowanie (konstruktor kopiujący)

    BankAccount other_copy_of_acc(account1);  // kopiowanie (konstruktor kopiujący)

    BankAccount another_copy_of_acc{account1}; // kopiowanie (konstruktor kopiujący)

    BankAccount account2{2, 3000.0, "Adam Nowak"};

    account1 = account2; // kopiowanie (przypisanie kopiujace);
}

Jeśli klasa pozyskuje zasób i odwołuje się do niego przy pomocy wskaźnika (resource handle) operacja kopiowania wartościowego doprowadzi do współdzielenia zasobu. Wywołanie destruktorów obiektów współdzielących zasób doprowadzi do próby wielokrotnego zwolnienia zasobu (undefined behaviour). Kopiowanie takich obiektów powinno zostać zaimplementowane przez twórcę klasy za pomocą specjalnych operacji kopiujących:

  • konstruktora kopiującego - T(const T&)

  • kopiującego operatora przypisania - T& operator=(const T&)

class Vector
{
private:
    size_t size_;
    double* items_;
public:
    Vector(size_t size, double value);
    ~Vector();

    Vector(const Vector& source); // konstruktor kopiujący
    Vector& operator=(const Vector& source); // kopiujący operator przypisania

    //...
};

Konstruktor kopiujący

Poprawna implementacja konstruktora kopiującego dla klasy Vector powinna zaalokować odpowiednio duży blok pamięci, a następnie skopiować wszystkie elementy tablicy z obiektu źródłowego:

Vector::Vector(const Vector& source)
    : size_{source.size_}, items_{new double[source.size_]}
{
    for(size_t i = 0; i < size_; ++i)
        items_[i] = source.items_[i];
}

Kopiujący operator przypisania

Równolegle z implementacją konstruktora powinien zostać zaimplementowany kopiujący operator przypisania:

Vector& Vector::operator=(const Vector& source)
{
    if (&source != this)
    {
        double* temp = new double[source.size_];

        for(size_t i = 0; i != source.size_; ++i)
            items_[i] = source.items_[i];

        delete[] items_;  // zwolnienie pamięci po starej tablicy

        items_ = temp;
        size_ = source.size_;
    }

    return *this;
}

Przenoszenie

Kopiowanie może być kosztowne zwłaszcza w kontekście tworzenia obiektów tymczasowych lub zwracania obiektów w funkcji.

Vector operator+(const Vector& a, const Vector& b)
{
    if (a.size() != b.size())
        throw VectorSizeMismatch();

    Vector result(a.size());
    for(size_t i = 0; i != a.size(); ++i)
        result[i] = a[i] + b[i];

    return result;
}

// ...

void f(const Vector& x, const Vector& y, const Vector& z)
{
    Vector result;
    // ...
    result = x + y + z;
    // ...
}

W przypadku tworzenia obiektów tymczasowych nie chcemy ich kopiowania. Zamiast tego możemy dokonać transferu ich stanu wewnętrznego do obiektu docelowego. Jest możliwe z wykorzystaniem specjalnych operacji przenoszących (move operations):

  • konstruktora przenoszącego - T(T&&)

  • przenoszącego operatora przypisania - T& operator=(T&&)

Obie funkcje specjalne przyjmują jako argument r-value referenję (&&). Ten typ referencji może być wiązany tylko z obiektami r-value. W uproszczeniu obiekt r-value to obiekt tymczasowy, z którego bezpiecznie możemy pobrać zawartość pozostawiając go w stanie nieokreślonym, ale bezpiecznym do zniszczania.

class Vector
{
    // ...

    Vector(const Vector& source); // konstruktor kopiujący
    Vector& operator=(const Vector& source); // kopiujący operator przypisania

    Vector(Vector&& source); // konstruktor przenoszący
    Vector& operator=(Vector&& source); // przenoszący operator przypisania

    //...
};

Konstruktor przenoszący

Vector::Vector(Vector&& source)
    : size_{source.size_}, items_{source.items_}
{
    source.size_ = 0;
    source.items_ = nullptr;
}

Przenoszący operator przypisania

Vector& Vector::operator=(Vector&& source)
{
    if (&source != this)
    {
        size_ = source.size_;
        items_ = source.items_;

        source.size_ = 0;
        source.items_ = nullptr;
    }

    return *this;
}

Specjalne funkcje składowe klas

Specjalne funkcje składowe klas w C++11:

  • Konstruktor domyślny

  • Destruktor

  • Operacje kopiowania - konstruktor kopiujący i kopiujący operator=

  • Operacje przenoszenia - konstruktor przenoszący i przenoszący operator=

Wszystkie specjalne funkcje składowe są generowane przez kompilator i posiadają następujące cechy:

  • są publiczne

  • są inline

  • są non-explicit

C++11 daje możliwość jawnego zadeklarowania funkcji specjalnych jako domyślnych lub usunięcia ich z interfejsu klasy.

Domyślne specjalne funkcje składowe - default

Deklaracja default - wymusza na kompilatorze generację domyślnej implementacji dla deklaracji specyfikowanej przez użytkownika (np. generacja domyślnego konsktruktora w przypadku, gdy istnieją inne konstruktory przyjmujące parametry)

class Gadget
{
public:
    Gadget(const Gadget&); // copy constructor will prevent
                           // generating implicitly declared
                           // default ctoe and move operations

    Gadget() = default;
    Gadget(Gadget&&) noexcept = default;
};

Operacje zadeklarowane jako default są traktowane jako user-declared. W efekcie klasa:

class Any // default copy semantics enabled
{

};

nie jest taka sama jak klasa zaimplementowana w poniższy sposób:

class Any  // default copy semantics deprecated in C++14 (and later probably disabled)
{
    ~Any = default;
};

Usunięte funkcje składowe - delete

Deklaracja delete - usuwa wskazaną funkcję lub funkcję składową z interfejsu klasy. Nie jest generowany kod takiej funkcji, a wywołanie jej, pobranie adresu lub użycie w wyrażeniu z sizeof jest błędem kompilacji.

// prevents object from making copies and from move operations
class NoCopyable
{
protected:
    NoCopyable() = default;

public:
    NoCopyable(const NoCopyable&) = delete;
    NoCopyable& operator=(const NoCopyable&) = delete;
};


//  The same efect as NoCopyable
class NoMoveable
{
    NoMoveable(NoMoveable&&) = delete;
    NoMoveable& operator=(NoMoveable&&) = delete;
};

Usunięcie funkcji umożliwia uniknięcie niejawnej konwersji argumentów wywołania funkcji:

void integral_only(int a)
{
    cout << "integral_only: " << a << endl;
}

void integral_only(double d) = delete;

// ...

integral_only(10); // OK

short s = 3;
integral_only(s); // OK - implicit conversion to short

integral_only(3.0); // error - use of deleted function