S2-02 Multiparadigma programozás és Haladó Java 1

Tartalom

  1. Memóriakezelés: referencia- és érték-szemantika
  2. Referenciakezelési technikák, Objektumok másolása, move-szemantika
  3. Erőforrásbiztos programozás, RAII, destruktor és szemétgyűjtés
  4. Kivételkezelés, kivételbiztos programozás
  5. A konkurens programozás alapelemei Javában és C++-ban
  6. További források

1. Memóriakezelés: referencia- és érték-szemantika

Ebben a fejezetben "referencia" alatt C++ referenciákat és pointereket értünk. Azaz bármit ami memóriacímet tárol (Java-ban is referenciának hívják a pointereket)

Változók viselkedésének két koncepciója

  1. Hatáskör (scope)
  2. Élettartam (life)

Normális változónak: hatásköre és élettartama van

Referenciának: csak hatásköre van, élettartamot nem követ

Érték-szemantika (Value semantics)

Előnyök:

Referencia-szemantika (Reference semantics)

Előnyök:

Visszatérés referenciával

#include <iostream>

int& f()            // Függvény ami referenciát ad vissza
{
    int i = 300;    // Lokális változó a stack-en
    return i;       // Visszatérés lokális i változó címével
}   // HIBA: mire f függvény végetért, lokális i változó kiment a scope-ból
    //       és törlődött!

int main()
{
    int& x = f();

    // FUTÁSIDEJŰ HIBA: az alábbi sor "0"-t fog kiírni "300" helyett!
    std::cout << x << std::endl;

    return 0;
}

Copy elision

  1. Inicializációkor

    T x = T(T(T()));    // Ehelyett T x = T()
    
    // Nem fogja a rakás copy-konstruktort meghívni,
    // csak egy alapértelmezett konstruktort.
  2. Függvényhívásban

    T f() { return T(); }   // Visszatérés temporary-val
    
    int main()
    {
        T x = f();              // Ehelyett T x = T()
        T* p = new T(f());      // Ehelyett T* p = new T()
    }

2. Referenciakezelési technikák, Objektumok másolása, move-szemantika

Referenciakezelési technikák

Memóriakezelés

Memória szegmens: Operációs rendszer \(\Rightarrow\) minden programnak egy területet tart fenn a memóriából.

Memóriacím: Minden bájt (memória hely) rendelkezik egy sorszámmal. Ezen keresztül elérhető a memória szegmensben. (Általában hexadecimális, pl.: 0x34c420)

Referencia

Egy változó memóriacímét az & operátorral kérdezhetjük le, ez a referencia operátor. (&<változónév> a változó első bájtjának memóriabeli címe).

int i = 128;
int j = i;  // egyszerű változó
int& k = i; // referencia változó
Referencia változók

Referencia változók

Pointerek

Speciálisabb változótípus: memóriacímet tárol értékként.

Memóriaterületek

Használat szempontjából háromféle memóriaterületet különböztetünk meg:

Memóriahely felszabadítás

A lefoglalt memóriát fel is kell szabadítani.

Konstans mutatók, referenciák

Módosíthatatlanná tehetjük a referenciákat a pointereket és az értékeket is:

(Trükk: Értelmezzük fordított sorrendben a deklarációkat)

double d1 = 10, d2 = 50;
double const_reference &d1r = d1;                         // konstans referencia
double const * pointer_to_const = &d1;                    // mutató konstansra
double * const const_pointer = &d1;                       // konstans mutató
// konstans mutató konstans értékre
double const * const const_pointer_to_const_value = &d1;  

const_reference = 100;                    // HIBA, az érték nem módosítható
*pointer_to_const = 50;                   // HIBA, az érték nem módosítható
*const_pointer = 50;                      // az érték módosítható
*const_pointer_to_const_value = 50;       // HIBA
pointer_to_const = &d2;                   // átállíthatjuk más memóriacímre
const_pointer = &d2;                      // HIBA, a mutató nem állítható át
const_pointer_to_const_value = &d2;       // HIBA

Konstruktor, Destruktor

Típusok mezői is lehetnek mutatók, melyeknek dinamikusan allokálhatunk memóriaterületet. Ezt a konstruktorban végezzük.

A törlésről viszont gondoskodnunk kell. Ezt megtehetjük a destruktorban

class <típus> {
    public:
    <typusnév>() { … } // konstruktor
    ~<típusnév>() { … } // destruktor
    …
};

Objektumok másolása

Kétféle másolási megközelítés ismert:

Példányok másolását két művelet teszi lehetővé: másoló konstruktor, értékadó operátor

Copy-konstruktor

Egy létező példány alapján újat hoz létre. Paraméterként egy másik (ugyanolyan típusú) példány referenciáját kapja, ennek a mezőit másolja le. (Ha nincs dinamikus tartalom, akkor az alapértelmezett megfelelő.)

class MyType {
    private:
        int* _value;
    public:
        MyType(const MyType& other) { // másoló konstr.
            _value = new int;

            // a dinamikus taralom létrehozása
            *_value = *other._value; // érték másolása
        }
};

Értékadó operátor

A kezdeti értékadást kivéve, amikor a változónak értéket adunk, az értékadó operátor lép érvénybe.

Megkapja a másolandó példány (konstans) referenciáját, és biztosítja taralmának átmásolását.

class MyType {
    public:
    …
        MyType& operator=(const MyType& other){
            if (this == &other)
                // ha ugyanazt a példányt kaptuk
                return *this; // nem csinálunk semmit

            *_value = *(other._value);
            // különben a megfelelő módon másolunk
            return *this; // visszaadjuk a referenciát
        }
};

Paraméterátadás

Ahogy a változókat, úgy a paramétereket is háromféleképpen tudjuk átadni:

Az érték szerinti átadás sokszor költséges lehet, mert ekkor a paraméterek másolódnak. A pointer és a referencia közötti döntést pedig az határozza meg, hogy míg a pointerek felvehetik a NULL értéket, addig a referenciák nem.

Tehát a következők szerint érdemes a paraméterátadást használni:

Move-szemantika

A C++-ban értékszemantika van. Ez egy tiszta memóriaterület szeparációt tud eredményezni, de sokszor teljesítményromlást okozhat nagy objektumok másolása esetén.

Tekintsük a következő Array implementációt:

class Array{
    public:
        Array (const Array&);
        Array& operator=(const Array&);
        ~Array ();
    private:
        double *val;
};
Array operator+(const Array& lhs, const Array& rhs){
    Array res = left;
    res += right;
    return res;
}

Az Array egy osztály, melynek + operátora összekonkatenálja a két paramétert és visszaad egy új listát.

A következő függvény meghívásánál azonban több köztes Array példány keletkezik és szűnik meg:

void f()
{
 Array b, c, d;
 …
 Array a = b + c + d;
}

A move-szemantika az ehhez hasonló problémákra ad megoldást.

Ehhez kell:

RValue, LValue

Korábbi nyelvekben értékadás: <variable> = <expression> (pl.: x = 5)

C/C++-ban értékadás: <expression> = <expression> (pl.: *++ptr = *++qtr)

Lvalue:

Rvalue:

Rvalue referencia operátor (&&)

A && operátorral lehet kasztolni Lvalue-t Rvalue-vá. (balértékből jobbértéket)

Példa:

struct S
{
 S() { a = ++cnt; std::cout << "S()" << std::endl; }
 S(const S& rhs) { a = rhs.a; std::cout << "copyCtr" << std::endl; }
 S(S&& rhs) { a = rhs.a; std::cout << "moveCtr"<< std::endl; }
 S& operator=(const S& rhs) { a = rhs.a; std::cout << "copy=" << std::endl; return *this; }
 S& operator=(S&& rhs) { a = rhs.a; std::cout << "move=" << std::endl; return *this; }
 int a ;
 static int cnt;
};
int S::cnt = 0;

int main()
{
 S a, b;
 swap( a, b);
}

Move-operátor használata nélkül:

template<class T>
void swap(T& a, T& b)
{
    T tmp(a);
    a = b;
    b = tmp;
}

Eredmény:

S()       // S a
S()       // S b
copyStr   // T tmp(a)
copy=     // a = b
copy=     // b = tmp

Move-operátor használatával:

template<class T>
void swap(T& a, T& b)
{
    T tmp(std::move(a));
    a = std::move(b);
    b = std::move(tmp);
}

Eredmény:

S()       // S a
S()       // S b
moveCtr   // T tmp(std::move(a))
move=     // a = std::move(b)
move=     // b = std::move(tmp)

Perfect forwarding

template<class T>
void wrapper(T&& arg)
{
    // arg mindig Lvalue lesz
    foo(std::forward<T>(arg)); // T-től függően arg továbbítása Lvalue- vagy RValue-ként
}

Például saját make_unique írása (unique_ptr nem másolható):

template<class T, class U>
std::unique_ptr<T> my_make_unique(U&& u)
{
    return std::unique_ptr<T>(new T(std::forward<U>(u)));
}

3. Erőforrásbiztos programozás, RAII, destruktor és szemétgyűjtés

Resource Allocation is Initialization-elv (RAII)

Példák:

Smart pointerek általában

Auto pointer (std::auto_ptr)

Unique pointer (std:unique_ptr)

Shared pointer (std::shared_ptr)

Weak pointer (std::weak_ptr)

Szemétgyűjtés (Garbage Collection, GC) Java-ban

(Nem találtam Multiparadigma programozás tananyagában C++ garbage collector-okról szóló részt. Ha mégis kell beszélni valamit róla, akkor a Boehm garbage collector-t érdemes megemlíteni.)

Referenciaszámláló szemétgyűjtés (reference counting)

Mark and Sweep szemétgyűjtés

Generációs szemétgyűjtés (Generational)

4. Kivételkezelés, kivételbiztos programozás

Hibakezelés

Hiba: program futása alatt bekövetkezett nemkívánt állapot. Két fajtáját különböztetjük meg:

Hibakezelés alatt a runtime error-okkal foglalkozunk.

Hibakezelés klasszikus C-ben

Kivételkezelés alapköve C-ben: jump műveletek

  1. jmp_buf x; - reprezentálja az elmentett stack állapotot
  2. setjmp(x) - elmenti a stack állapotát, illetve ide ugrik vissza a vezérlés longjmp hívása után
  3. longjmp(x, 5) - kiváltja a stack visszaállítását az x által reprezentált állapotba. Mellékel egy hibakódot is, melyet a setjmp visszaad.

Használat:

#include <setjmp.h>
#include <stdio.h>

jmp_buf x;

void f()
{
    longjmp(x,5);
}

int main()
{
    int i = 0;

    if ( (i = setjmp(x)) == 0 )
    {
        f();
    }
    else
    {
        switch( i )
        {
        case  1:
        case  2:
        default: fprintf( stdout, "error code = %d\n", i); break;
        }
    }
    return 0;
}

Érezhetőek a következő megfeleltetések:

Static Assert (C++11)

Fordítási időben kiszámolható boolean kifejezések (bool_constexpr) igazságértékét vizsgálja

Kivételkezelés

Kivételkezelés célja

try-catch

try {
    f();
    // ...
}
catch (T1 e1) { /* handler for T1 */ }
catch (T2 e2) { /* handler for T2 */ }
catch (T3 e3) { /* handler for T3 */ }

Hierarchia

Továbbra sem ajánlott new-val kivételt létrehozni. Ha a dianamikus típussal akarunk játszani, inkább használjuk a következő megoldást:

struct ExceptionBase{
    virtual void raise() { throw *this; }
    virtual ~ExceptionBase() {}
};

struct ExceptionDerived : ExceptionBase{
    virtual void raise() { throw *this; }
};

void foo(ExceptionBase& e){
    e.raise(); // Uses dynamic type of e while raising an exception.
}

int main (void){
    ExceptionDerived e;
    try {
        foo(e);
    }catch (ExceptionDerived& e) {
        ...
    }catch (...) {
        ...
    }
}

Kivételkezelés és osztályok

Kérdéses esetek: konstruktor, destruktor

class X
{
    public:
        X(int i) { p = new char[i]; init(); }
        ~X() { delete [] p; }       // must not throw exception
    private:
        void init() { ... throw ... }   // BAD: destructor won't run !
        char *p;                        // constructor was not completed
    };

Ha tagváltozó inicializálása dob hibát, akkor dob a konstruktor is

class X
{
    public:
        X() { throw 1; }
};
class Y
{
    public:
        Y()
        try
            : x()
        { }
        catch( ... ) { /* throw; */ }
    private:
        X x;
};
int main(){
    try {
        Y y;
        return 0;
    }
    catch (int i)
    {
        std::cerr << "exception: " << i << std::endl;
    }
}

Noexcept (C++11)

Kétféle formában létezik:

    template <typename T>
    void f() noexcept ( noexcept( T::g() ) )
    {
      g();
    }

Magyarázat: noexcept( T::g() ) - operátor formás noexcept, megmondja, hogy g() dob-e exception-t

5. A konkurens programozás alapelemei Javában és C++-ban

Problémák a C++98 memóriamodellel

C++11 memóriamodell

Hogyan írjunk szálbiztos Singleton-t C++11-től?

C++11 memóriamodellje már garantálja, hogy lokális statikus változók szálbiztosan jönnek létre

class Singleton   // Meyers Singleton, nevét Scott Meyers-ről kapta
{
public:
    Singleton(const Singleton&) = delete;
    Singleton(Singleton&&) = delete;
    Singleton& operator=(const Singleton&) = delete;
    Singleton& operator=(Singleton&&) = delete;
    static Singleton& getInstance() const
    {
        return _instance;
    }
private:
    Singleton() {}
    static Singleton _instance;
};

std::thread

std::mutex

std::lock_guard

std::atomic<T>

Párhuzamos programozás C++-ban (std::async, std::future, std::promise)

Konkurens és párhuzamos programozás Java-ban

Szálak:

Alapvető szinkronizáció:

Párhuzamos programozás

6. További források