Destructori și virtualizare

În general, două situații

Destructori protected și non-virtuali

Utilizați dacă vrem să ținem într-o clasă de bază comună funcții și atribute, dar construim doar obiecte derivate și nu avem nevoie de pointeri/referință la bază.

Nu este public pentru a preveni instanțierea bazei din exterior (nu avem object slicing).

class Base
{
protected:
    ~Base() = default;
};

int main()
{
    Base base; // eroare de compilare, nu se poate instanta
    Base* base1; // merge, doar se aloca pointer
}

Problemă

class Instrument
{
public:
    virtual ~Instrument() = 0;
    virtual void play() const = 0;
};

Instrument::~Instrument() {}

class Violin : public Instrument
{
public:
    void play() const override;
};

class Drums: public Instrument
{
public:
    void play() const override;
};

class Orchestra
{
    std::vector<Instrument*> instruments;
public:
    void add(Instrument* instrument);
    void rehearse() const;
};

void Orchestra::add(Instrument* instrument)
{
    instruments.push_back(instrument);
}

void Orchestra::rehearse() const
{
    for (const auto& instrument : instruments)
    {
        instrument->play();
    }
}

int main()
{
    Orchestra o1;
    o1.add(new Drums);
    o1.add(new Violin);
    o1.rehearse();
    return 0;
}

Prima problemă -> memory leaks, nu se dezalocă niciodată pointerii alocați în Orchestra.

idee -> destructor pentru Orchestra, care dezalocă pointerii. Duce la altă problemă, la copiere implicită de obiecte , se face shallow copy (sunt copiate adresele pointerilor), lucru ce va duce la ștergerea acelorași zone din memorie de mai multe ori (nu e eroare de compilare, dar risky)

idee -> constructor de copiere, cu static_cast sau dynamic_cast

if (dynamic_cast<Violin*>(ptr)) return new Violin(*ptr);
else if (dynamic_cast<Drums*>(ptr)) return new Drums(*ptr);

Very bad design

Soluție 1

Nu se mai permit copieri, se folosesc doar mutări. Doar un obiect Orchestra deține obiectele.

Disable copy:

Orchestra(const Orchestra&) = delete;
Orchestra& operator=(const Orchestra&) = delete;

Allow move operations

Orchestra& operator=(const Orchestra& other) = delete;
Orchestra(Orchestra&& other) = default;
Orchestra& operator=(Orchestra&& other) = default;

Soluție 2

Funcție de clone, fiecare subclasă ar trebui să știe să se copieze pe sine.

class Instrument
{
public:
    virtual ~Instrument();
    virtual void play() const = 0;
    virtual Instrument* clone() const = 0;
}

class Violin : public Instrument
{
public:
    void play() const override;
    Instrument* clone() const override;
};

Instrument* Violin::clone() const
{
    return new Violin(*this);
}

Deci Orchestra virtuala permite copiere:

Orchestra::Orchestra(const Orchestra& other)
{
    for (const auto& instrument : other.instruments)
    {
        instruments.push_back(instrument->clone());
    }
}

Și destructor:

Orchestra::~Orchestra()
{
    for (const auto& instrument : instruments)
    {
        delete instrument;
    }
}

Regula celor trei

Dacă într-o clasă trebuie să suprascriem constructor de copiere, operatorul = sau destructor, cel mai probabil trebuie suprascrise toate cele trei funcții.

Copy and swap

Pentru a evita starea invalidă, operatorul de atribuire trebuie să folosească copy-and-swap

Orchestra& Orchestra::operator=(const Orchestra& other)
{
    if (this == &other)
        return *this;
    auto copie = other; // constructor copiere
    std::swap(instruments, copie.instruments);
    return *this;
} // aici destructor copie

Partea de swap poate fi refolosită, motiv pentru care se poate defini o funcție separată:

friend void swap(Orchestra& o1, Orchestra& o2) {
    using std::swap;
    swap(o1.instruments, o2.instruments);
}

Resource acquisition is initialization (RAII)

Ideea fundamentală de gestionare a resurselor în C++ e că resursele ar trebui alocate doar în constructori și dealocate doar în destructori.

De ce? -> pentru că destructorii și constructorii sunt apelați automat de limbaj. Dacă nu se fac alocări/dezalocări, memory leak-urile sunt imposibile.

Nu există garbage collection, RAII este suficient.

Exemplu de RAII: smart pointers

Dezavantaj smart pointers: nu putem folosi tipuri de date covariante.

Tipuri de date covariante

class Baza {
public:
    virtual ~Baza() = default;
    virtual Baza* clone() const = 0; // funcție virtuală pură
};

class Derivata1 : public Baza {
public:
// tip covariant (Derivata1* în loc de Baza*)
    Derivata1* clone() const override {
        return new Derivata1(*this);
    }
    void f() { std::cout << "f der1\n"; }
};

class Derivata2 : public Baza {
public:
    Derivata2* clone() const override {  // tip covariant
        return new Derivata2(*this);
    }
    void g() { std::cout << "g der2\n"; }
};

int main() {
    Baza* b1 = new Derivata1;
    // Derivata1* d1 = b1->clone();  // eroare -> nu este cast explicit
    // chiar daca prin virtual ar apela functia buna, virtual nu poate schimba tipul static al b1->clone(), care, in baza, este Base*
    // b1->f();  // eroare
    delete b1;
    Baza* b2 = new Derivata2;
    Derivata2 d2;
    // Derivata2* d2_1 = b2->clone();  // eroare
    Derivata2* d2_2 = d2.clone();  // ok
    d2_2->g();  // ok
    delete b2;
    delete d2_2;
}

Interfață non-virtuală (NVI)

Toate clasele derivate au o implementare comună și trebuie să suprascrie doar anumite porțiuni.

class Base {
public:
    // NVI
    void process() { // nevirtuala
	    preProcess()
        doProcess();
    }

protected:
    virtual void doProcess() = 0;  // customization point

private:
    void preProcess() {
        // setup code 
    }
};

class Derived1 : public Base {
protected:
    void doProcess() override {
        // customized behavior here 
    }
};

class Derived2 : public Base {
protected:
    void doProcess() override {
        // customized behavior here
    }
};

Prin această implementare este mult mai ușor să modificăm structura implementării la nivelul întregii ierarhii.

NVI asigură ca apelul se face doar prin funcția publică ne-virtuală -> clasele derivate nu pot evita implementarea comună dată de clasa de bază.

Tratarea excepțiilor în C++

Excepțiile (în C++) pot fi cauzate de:

Sintaxă:

try {
// try block
} catch (type1 arg) {
// catch 1
} catch (type2 arg) {
// catch 2
} 
...
catch(typeN arg) {
// catch N 
}
Important

Tipul argumentului din catch arată care bloc este executat.

Dacă nu este generată excepție ⟹ nu se execută bloc catch.

Instrucțiunile catch sunt verificate în ordine, fiind executat primul de tipul erorii.

Observații

void customTerminate() {
    std::cerr << "Custom terminate handler calleds\n";
    std::abort(); 
}

int main() {
    std::set_terminate(customTerminate);

    throw 42;  // No catch, triggers terminate()
    return 0;
}

Excepții standard

Toate se moștenesc din std::exception.

Multe din ele sunt în <exception> sau <stdexcept>.

try {
        // Code that may throw
    }
    catch (const std::exception& e) {  // Catch any standard exception
        std::cerr << "Caught exception: " << e.what() << std::endl;
    }

Unde .what() întoarce un string care detaliază eroarea.

Aruncarea erorilor din clase de bază/derivate

Important

Un catch pentru tipul de bază va fi executat și pentru un obiect din tipul derivat.

Deci, catch-ul din tipul derivat trebuie pus primul și apoi catch-ul de bază.

class B{};
class D : public B {};

int main()
{
	D derived;
	try {
		throw derived;
	} catch (const B& b) {
		cout << "caught base" << endl;
	} catch (const D& d) {
		cout << "caught derived" << endl;
	} // va afisa caught base
}

Când să aruncăm excepții?

Ce punem în catch?

C++ specific

C++ permite definirea unei ierarhii de erori de la zero

class MyCustomError : public std::runtime_error {
public:
    explicit MyCustomError(const std::string& message)
        : std::runtime_error(message) {}
};

Observații: