vector::push_back I.

Czirkos Zoltán · 2022.06.21.

Miért másolja az std::vector push_back() függvénye előbb az új adatot, és csak utána a régieket?

1. Előbb az új, aztán a régiek

Az előadáson szerepelt a Noisy osztály, amelynek konstruktoraival láthatóvá vált az std::vector memóriakezelése. Volt egy ehhez hasonló kódrészlet:

std::vector<Noisy> v(3);

Noisy n{5};
v.push_back(n);

Ebben a push_back()-es sor hatása a következőképpen jelent meg:

Noisy copy ctor 5   !
Noisy copy ctor 0
Noisy copy ctor 0
Noisy copy ctor 0
Noisy dtor 0
Noisy dtor 0
Noisy dtor 0

A lényeg a felkiáltójellel jelzett résznél van. Látjuk, hogy a vektor átméretezi a dinamikus tömböt (ezért volt szükség a három régi objektum másolására, és az eredeti tömb felszabadításakor a destruktoruk hívására). De vajon az új tömbbe miért a push_back()-elt objektum kerül be előbb, miért nem a régiek?

Máshogy is föltehetnénk a kérdést. Tekintsük a push_back() függvény alábbi megvalósítását. (Ez nem tartalmazza azt a haladó memóriakezelési technikát, amit az előadás mutatott be.)

template <typename T>
void vector<T>::push_back(T const & val) {
    /* helyet kell csinálni: eggyel nagyobb tömb kell */
    T* newdata = new T[size + 1];
    for (size_t i = 0; i < size; ++i)
        newdata[i] = data[i];
    delete[] data;
    data = newdata;

    /* új adat bemásolása */
    data[size] = val;
    size++;
}

A kérdés: hol a hiba? Mert tényleg van benne hiba.

A kérdést harmadikféleképp is föl lehet tenni. Ez a verzió a következőképpen hangzik: mi az a teszteset, amire a push_back() fenti megvalósításának a hibája előjön?

2. Hol a hiba?

A hiba a referencia típusú paraméter miatt jöhet elő. Adott a következő kódrészlet:

std::vector<std::string> v = { "alma", "körte", "barack" };

v.push_back(v[0]);

Mit várunk ettől a kódtól? Azt, hogy a vektor elején lévő sztringet, az almát, a vektor végéhez fűzze. Tehát az „alma, körte, barack, alma” szavakat tartalmazó vektort kellene kapnunk. Ezzel szemben a kódunk könnyen lehet, hogy csúfosan el fog szállni.

Mi történik ebben az esetben? Nagyon fontos a műveletek sorrendje:

  1. Meghívódik az indexelő operátor. Ez visszatér a vektor elején lévő objektum referenciájával.
  2. Ezek után a push_back() függvénybe kerülünk. Ennek szintén referencia típusú a paramétere; a val referencia ugyanahhoz az objektumhoz kötődik, mint amivel a v[0] visszatért, tehát a tömb elején lévő elemhez.
  3. A push_back() nekiáll átméretezni a vektort. Ehhez létrehoz egy nagyobb tömböt, és átmásolja az adatokat.
  4. Ezek után pedig felszabadítja a régi tömböt. A baj itt van: ezzel megszűntek azok az objektumok, tehát a paraméterként kapott referencia is érvénytelenné válik, „invalidálódik”. Az a régi tömbbeli objektumhoz kötődött! A referencia nem objektum, csak egy hivatkozás az objektumra, amely most eltűnt.

3. Hogy lehet javítani?

Hogy oldható meg ez a probléma könnyen? A megoldás egyszerű: a műveletek átsorrendezésével. Az új tömbbe előbb kell bemásolni a paraméterként kapott objektumot, vagy legalábbis a felszabadítást annak bemásolása után szabad csak megtenni. Ugyanis nem lehet tudni, hogy a referenciával átvett objektum nem valamelyik tömbelem-e véletlenül.

A klasszikus memóriakezeléssel így kell kinéznie a push_back() függvénynek:

template <typename T>
void vector<T>::push_back(T const & val) {
    /* helyet kell csinálni: eggyel nagyobb tömb kell */
    T* newdata = new T[size + 1];

    /* régi adatok átmásolása, új adat bemásolása */
    for (size_t i = 0; i < size; ++i)
        newdata[i] = data[i];
    newdata[size] = val;

    /* most már mehet a delete[] */
    delete[] data;
    size++;
    data = newdata;
}

Ezzel nem magyaráztuk meg teljesen, miért kell előbb másolni az új adatot. De arról majd egy kicsit később, egy másik írásban: push_back, 2. rész.