Haladó memóriakezelés

Czirkos Zoltán · 2022.06.21.

Placement new, osztályok saját operator new függvényei, allokátorok.

Ez a labor néhány haladó memóriakezelési technikát mutat be.

Feltöltés

A laborhoz kiírt beadandó a szokásos helyen van, az admin portálon. Ide óra végén töltsd fel a forráskódokat (*.cpp, *.h)! A feladatokat ezért külön projektben oldd majd meg, ne írd felül a megoldásokat.

Labor otthoni munkában

A labor teljesítéséhez mind a három feladatot meg kell oldani.

1. A globális operator new

Emlékezz vissza az előadásra a globális operator new függvénnyel kapcsolatban! A C++ szabványos könyvtár biztosít két függvényt a dinamikus memória foglalásához (allocation) és felszabadításához (deallocation):

#include <new>

void * operator new(size_t);
void operator delete(void *) noexcept;

Ezek a C-s malloc() és free() függvénnyel teljesen analóg módon használhatóak. Az operator new megkapja bájtokban, mekkora memóriaterületet kell foglalnia, és a foglalt területre mutató pointerrel tér vissza. Az operator delete az így kapott pointer kapja, és felszabadítja a memóriaterületet. Objektumok konstruktorát és destruktorát ezek nem futtatják, hanem csak a memória kezeléséért felelnek.

Ezek a függvények a felhasználó által definiálhatóak, és így a rendszer által adott dinamikus memóriakezelő lecserélhető.

  1. Próbáld ezt ki! Definiáld a fenti függvényeket úgy, hogy a C-s könyvtári malloc() és free() függvényeket használják!
  2. Írj mindegyik függvénybe egy kiírást a működés megfigyeléséhez, pl. „allocated 45 bytes at 0x01203567” és „freed memory at 0x01203567”. A void * pointerekhez van operator<< overload.
  3. Foglalj egy int-et: new int. Szabadítsd fel: mit látsz?
  4. Hozz létre egy std::string objektumot: std::string h = "helló világ alma körte barack";. (Fontos, hogy hosszú szöveg legyen benne, mert egyes std::string implementációk a rövid szövegeknél el tudják kerülni a dinamikus memóriakezelést.) Mit látsz? És akkor, ha std::string *h = new std::string("helló világ alma körte barack") módon foglalod? Kipróbálhatsz más osztályt is, pl. std::set{1, 2, 3, 4, 5}.
  5. Valósíts meg egy egyszerű dinamikus memóriakezelést ellenőrző keretrendszert a függvények által! Tartsd nyilván egy közös (globális) int változóban, hány darab memóriaterület van épp lefoglalva! A cstdlib fejlécfájl atexit() függvényének átadhatsz egy paraméter és visszatérési érték nélküli, void() függvényt, amelyet a programból kilépéskor, azaz a main()-ből visszatérés után meghív. Adj ennek egy olyan függvényt, amely kiírja, hány darab felszabadítatlan memóriaterület maradt! Teszteld, hogy mutatja-e a felszabadítatlan területeket!
  6. A new operátornak std::bad_alloc típusú kivételt kell dobnia, ha a foglalás nem sikerült. Ellenőrizd ezért a malloc() által adott pointert, hogy nem null értékű-e! Dobj hibát, ha ez null, vagy ha 1 megabájtnál nagyobb területet próbál foglalni a hívó!
Megoldás
#include <iostream>
#include <cstdlib>
#include <string>
#include <set>
#include <stdexcept>
#include <new>

class MAllocator {
  private:
    static unsigned allocated_count;
  public:
    static void * alloc(size_t size) {
        if (size > 1<<20) {
            std::cerr << "Can't allocate " << size << " bytes" << std::endl;
            throw std::bad_alloc{};
        }
        ++allocated_count;
        void *mem = malloc(size);
        if (mem == nullptr) {
            std::cerr << "malloc() returned nullptr for " << size << " bytes" << std::endl;
            throw std::bad_alloc{};
        }
        std::cerr << "Allocated " << size << " bytes at " << mem << std::endl;
        return mem;
    }

    static void dealloc(void *mem) noexcept {
        --allocated_count;
        std::cerr << "Freed mem at " << mem << std::endl;
        free(mem);
    }

    static void allocated_report_atexit() {
        if (allocated_count != 0) {
            std::cerr << "Memory leak: " << allocated_count << " chunk(s) still allocated" << std::endl;
        }
    }
    
    static void init() {
        atexit(allocated_report_atexit);
    }
};

unsigned MAllocator::allocated_count = 0;


void * operator new(size_t size) {
    return MAllocator::alloc(size);
}

void operator delete(void *mem) noexcept {
    return MAllocator::dealloc(mem);
}

int main() {
    MAllocator::init();
    delete new int;
    std::cout << "=====\n";
    {
        std::string s = "hello vilag alma korte barack szilva dinnye";
    }
    std::cout << "=====\n";
    delete new std::string("hello vilag alma korte barack szilva dinnye");
    std::cout << "=====\n";
    delete new std::set<int>{1, 2, 3, 4, 5, 6, 7};
    std::cout << "=====\n";
    try {
        new int[123456789];
    } catch (std::bad_alloc &b) {
        std::cout << "Caught std::bad_alloc" << std::endl;
    }
    std::cout << "=====\n";
    new int;
    std::cout << "=====\n";
}

Implementációs részletek

Előfordulhat, hogy a fordító figyelmeztetést ad, miszerint saját operator delete(void*) implementáció esetén operator delete(void*, size_t) függvényt is kell írni. Az operator delete(void *, size_t) változatról előadáson külön nem esett szó. Ezt használhatja a fordító akkor, ha tudja valahonnan felszabadításkor a terület méretét is (ami nyilvánvalóan megegyezik a foglalt mérettel). Lényeg, hogy a kettőt együtt kell megvalósítani. Ha szeretnél később elmélyedni a témában, az operator delete(void*, size_t) függvényről szóló írásban olvashatsz erről.

Hasonlóképp, létezik külön operator new[] és operator delete[] is. Ha tömbök foglalásakor és felszabadításakor nem látod, hogy lenne kimenete a programodnak, írd meg ezeket is – akár a nem tömbös változatokra visszavezetve.

2. A „placement new” szintaxis

Emlékezz vissza az előadáson bemutatott „placement new” szintaxisra! Ez arra jó, hogy egy már lefoglalt, de még memóriaszemetet tartalmazó memóriaterületre „ráhívjunk” egy konstruktort, hogy ott egy objektum keletkezzen. Így lehet objektumot létrehozni memóriafoglalás nélkül, de természetesen a helyet nekünk kell biztosítanunk. Az így létrehozott objektum destruktorának hívásáért is a programozó felel, de az működik szokványos tagfüggvényként is:

class Foo { /* ... */ };

unsigned char mem[sizeof(Foo)];
Foo *f = new (mem) Foo{}; // ctor

f->do_something();

f->~Foo(); // dtor

A feladat egy olyan verem (stack) osztályt írni, amely:

  • push() tagfüggvénye megjegyzi a paraméterként kapott értéket,
  • pop() tagfüggvénye visszaadja a legutóbb beszúrt értéket (törölve azt a veremből),
  • a konstruktora átveszi a verem maximális méretét,
  • az elemeket pedig belül egy olyan tömbben tárolja, amelyet a malloc() függvénnyel foglalt.

A verem osztály másoló konstruktorát írd meg! Az értékadó operátorával ne töltsd az időt, azt csak tiltsd le. A példányosító típusnak ne használd az alapértelmezett konstruktorát vagy értékadó operátorát, sőt tételezd fel, hogy nincs is neki egyik sem! A teszteléshez használhatod az alábbi Noisy osztályt.

(Figyelem: ennek a feladatnak semmi köze az előzőhöz! Nem az operator new memóriafoglaló függvény átdefiniálásával kell megoldani, sőt azzal nem is lehet.)

A Noisy osztály kódja
class Noisy {
  public:
    explicit Noisy(int i) : i_{i} {
        std::cout << "Noisy{" << i << "} ctor\n"; count++;
    }
    Noisy(Noisy const &o) : i_{o.i_} {
        std::cout << "Noisy copy ctor " << i_ << "\n"; count++;
    }
    Noisy& operator=(Noisy const &o) = delete;
    ~Noisy() {
        std::cout << "Noisy dtor " << i_ << "\n"; count--;
        i_ = rand();    /* ! */
    }
    static void report() {
        std::cout << count << " instance(s).\n";
    }
  private:
    int i_;
    static int count;
};

int Noisy::count = 0;

A destruktor jelölt sorával kapcsolatban egy megjegyzés. Ezt az értékadást a fordító kioptimalizálhatja; minek is adnánk értéket egy tagváltozónak a destruktorban, ha ez az objektum meg fog szűnni mindjárt. A memóriaterületét senki nem kellene olvassa már az objektum élettartamának vége után. Ha mégis, akkor az egy hibás kódrészlet (definiálatlan működésű); azt pedig nem kell biztosítania a fordítónak, hogy a hibás kódra determinisztikus viselkedésű programot generáljon.

Megoldás
#include <iostream>
#include <cstdlib>
#include <new>
#include <stdexcept>

#define DEBUG 1


template <typename T>
class Stack {
  public:
    Stack(size_t max_size);
    Stack(Stack const &orig);
    Stack & operator=(Stack const &orig) = delete;
    ~Stack();
    void push(T const &what);
    T pop();
    bool empty() const;
  private:
    size_t size_;
    size_t max_size_;
    T *pData_;
};


template <typename T>
Stack<T>::Stack(size_t max_size) {
    size_ = 0;
    max_size_ = max_size;
    /* itt nem szabad new T[]-t irni, mert az lefuttatna max_size_ darab T
     * objektum default konstruktorat is. csak memoria kell, objektumok nincsenek. */
    pData_ = (T*) malloc(sizeof(T) * max_size_);
}


template <typename T>
Stack<T>::Stack(Stack<T> const &orig) {
    size_ = orig.size_;
    max_size_ = orig.max_size_;
    /* ez sem new T[], hanem csak malloc, hogy csak memoria legyen. egyelore. */
    pData_ = (T*) malloc(sizeof(T) * max_size_);
    /* itt masolodnak az objektumok is. ez nem lehet pdata[i] = orig.pdata[i], mert
     * pdata[i] meg memoriaszemet, es kulonben annak az operator=-jet hivnank! */
    for (size_t i = 0; i != size_; ++i)
        new (&pData_[i]) T{orig.pData_[i]};
}


template <typename T>
Stack<T>::~Stack() {
    /* destruktorok futtatasa */
    for (size_t i = 0; i != size_; ++i)
        pData_[i].~T();
    /* memoria felszabaditasa. ez nem lehet delete[]. */
    free(pData_);
}


/* Push element onto stack. */
template <typename T>
void Stack<T>::push(T const &what) {
    if (size_ + 1 > max_size_)
        throw std::length_error("stack tele");
    /* a memoria mar megvan, csak egyelore memoriaszemet van a tarolt elem helyen.
     * ezert nem operator=-t hivunk, hanem konstruktort! */
    new (&pData_[size_]) T{what};
    size_++;
}


/* Pop element from top of the stack. */
template <typename T>
T Stack<T>::pop() {
    if (size_ == 0)
        throw std::length_error("stack ures");
    T saved{pData_[size_-1]};
    /* itt lefut a destruktor, de a memoriaterulet marad! az kulon lett malloc()-olva. */
    pData_[size_-1].~T();
    size_--;
    return saved;
}


template <typename T>
bool Stack<T>::empty() const {
    return size_ == 0;
}


int main() {
    Stack<char> s{100};
    char c;
    while (std::cin.get(c))
        s.push(c);
    while (!s.empty())
        std::cout << s.pop();
}

3. Verem nyújtózkodó tömbbel

Írd át a fenti vermes feladat osztályát úgy, hogy ne kelljen megadni maximális méretet! Ha kell, a verem méretezze át a tároláshoz használt tömböt az alábbi módon:

  • Ha teli verembe kell beszúrni, megduplázza a méretét,
  • ha kiszedés után negyedére zsugorodott a használt terület a foglalthoz képest, akkor pedig felezze meg a méretét.
Megoldás
#include <iostream>
#include <cstdlib>
#include <new>
#include <stdexcept>

#define DEBUG 1


template <typename T>
class Stack {
  public:
    Stack();
    Stack(Stack const &orig);
    Stack & operator=(Stack const &orig) = delete;
    ~Stack();
    void push(T const &what);
    T pop();
    bool empty() const;
  private:
    size_t size_;
    size_t allocated_;
    T *pData_;
    void resize(size_t new_allocated);
};


template <typename T>
Stack<T>::Stack() {
    size_ = 0;
    allocated_ = 1;
    pData_ = (T*) malloc(sizeof(T) * allocated_);
}


template <typename T>
Stack<T>::Stack(Stack<T> const &orig) {
    size_ = orig.size_;
    allocated_ = orig.allocated_;
    pData_ = (T*) malloc(sizeof(T) * allocated_);
    for (size_t i = 0; i != size_; ++i)
        new (&pData_[i]) T{orig.pData_[i]};
}


template <typename T>
Stack<T>::~Stack() {
    for (size_t i = 0; i != size_; ++i)
        pData_[i].~T();
    free(pData_);
}


/* Reallocate stack's objects, move data to new memory area. */
template <typename T>
void Stack<T>::resize(size_t new_allocated) {
    if (allocated_ == new_allocated || new_allocated < size_)
        return;
#if DEBUG
    std::cerr << "Resizing from " << allocated_ << " to " << new_allocated << std::endl;
#endif
    T *newPData = (T*) malloc(sizeof(T) * new_allocated);
    for (size_t i = 0; i != size_; ++i) {
        new (&newPData[i]) T{pData_[i]};
        pData_[i].~T();
    }
    free(pData_);
    allocated_ = new_allocated;
    pData_ = newPData;
}


/* Push element onto stack. */
template <typename T>
void Stack<T>::push(T const &what) {
    if (size_ + 1 > allocated_)
        resize(allocated_ * 2);
    new (&pData_[size_]) T{what};
    size_++;
}


/* Pop element from top of the stack. */
template <typename T>
T Stack<T>::pop() {
    if (size_ == 0)
        throw std::length_error("ures stack");
    T saved{pData_[size_-1]};
    pData_[size_-1].~T();
    size_--;
    if (size_ <= allocated_ / 4)
        resize(allocated_ / 2);
    return saved;
}


template <typename T>
bool Stack<T>::empty() const {
    return size_ == 0;
}


int main() {
    Stack<char> s;
    char c;
    while (std::cin.get(c))
        s.push(c);
    while (!s.empty())
        std::cout << s.pop();
}

4. További feladatok