Kihagyás

Operator overloading

operátor kiterjesztés

Az eddig ismert alap típusokra léteznek eleve definiált műveletek, operátorok. Egy egész típusú változó operátora lehet pl. összeadás (+), kivonás (-), értékadás (=), inkrementálás (++). Ezeknek a jól megszokott hatásuk van a változó értékére. Az általunk megvalósított osztályokra azonban alapból nem működnek ezek az operátorok. A C++ azonban lehetőséget kínál rá, hogy saját típusra is értelmezzük az operátorokat, csak meg kell mondanunk, hogy mi történjen pl. összeadás esetén. Ezt operátor kiterjesztésnek, vagy angolosan operator overloading hívjuk.

Vegyük alapul, hogyan működik pl. az összeadás operátor az egész számokra! Mit is jelent, ha leírjuk az a+b kifejezést:

1
a+b => össze kell adni: a, b => összead(a,b) => +(a,b) => operator+(a,b)

Fontos észrevenni, hogy az a és b értéke nem változik meg, és a kifejezés eredményének tárolására egy új ,,objektum'' jön létre (3+2 esetén 5, ami szintén egy egész), tehát van visszatérési értéke. Egész számok esetén ez egy egész szám lesz, tehát ha kiegészítjük a fentit a visszatérési értékkel, akkor a következő módon fog kinézni:

1
int operator+(a,b) => int operator+(int, int)

A saját osztályunkra nézve hasonlóan néz ki, csak meg kell határoznunk, hogy mit mivel szeretnénk összeadni és ennek mi lesz az eredménye. Ha vesszük a Kurzus osztályt és hozzáadunk egy Hallgato osztályt, akkor egy bővített kurzust kapunk, ami szintén egy Kurzus. Tehát a két paraméter: Kurzus, Hallgato (ami valójában konstans referencia lesz). A visszatérési érték típusa Kurzus.

1
Kurzus operator+(Kurzus, Hallgato)

Mivel az operátor viselkedése szorosan kötődik a Kurzushoz (így fogalmaztuk meg) OOP szerint az osztályon végzett műveletet rakjuk be az osztályba magába! Ezt a metódust megírhatjuk mint a Kurzus osztály tagfüggvénye. Ha tagfüggvény, akkor az első paraméter (Kurzus) adott is, hiszen ha meghívjuk egy objektum operator+ függvényét, az első paraméter maga az objektum, így a paraméterlistából azt el is hagyhatjuk.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
#define KURZUS_LIMIT 10

class Kurzus {
  string nev, kod;
  unsigned felvette = 0;
  Hallgato hallgatok[KURZUS_LIMIT];
public:
  Kurzus(const string& nev, const string& kod) : nev(nev), kod(kod) {
  }

  Kurzus operator+(const Hallgato& h) const { 
    // const, mert az eredeti kurzus objektum nem valtozik meg 
    Kurzus res = *this; //új kurzus objektum keletkezik
    if ( res.felvette == KURZUS_LIMIT ) {
      cout << "A kurzus megtelt. Nem lehet felvenni." << endl;
    } else {
      res.hallgatok[res.felvette++] = h;
    }

    return res; //egy új kurzussal térünk vissza
  }
};

Ez hozzáadja a Kurzus-hoz a kapott hallgatót, ha még elfér. Hasonló szemantikája van, mint az egész számoknál.

A használata a következő:

1
2
3
4
5
6
7
8
int main() {

  Kurzus kurzus("Programozas II.", "IB302G-1");
  Hallgato hallgato("Kreatív Név", "KNAA.SZE");

  kurzus = kurzus + hallgato;
  // a levezetésünk szerint valójában: kurzus = kurzus.operator+(hallgato);
}

Az egészeknél ha egy változóhoz hozzá akarunk adni egy értéket, az összeadást a következőképpen kell használnunk:

1
a = a + 5
Ennek rövidített változata:
1
a += 5
Ezt szintén megtehetjük a saját osztályunkra, ha az operator+=-t definiáljuk felül.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
const unsigned KURZUS_LIMIT = 10;

class Kurzus {
  string nev, kod;
  unsigned felvette = 0;
  Hallgato hallgatok[KURZUS_LIMIT];
public:
  Kurzus(const string& nev, const string& kod) : nev(nev), kod(kod) {
  }

  Kurzus& operator+=(const Hallgato& h) { // NEM konstans, az aktuális objektum módosul
    if ( felvette == KURZUS_LIMIT ) {
      cout << "A kurzus megtelt. Nem lehet felvenni." << endl;
    } else {
      hallgatok[felvette++] = h;
    }

    return *this;
  }
};

int main() {

  Kurzus k("Programozas II", "IB302G-1");
  Hallgato h("Kreatív Név", "KNAA.SZE");

  k += h1;
}

Ez a megvalósítás meglehetősen hasonló, azonban fontos kiemelni néhány jelentős különbséget! Az első, hogy a visszatérési érték nem Kurzus, hanem Kurzus& és nem hozunk létre egy újabb Kurzus típusú objektumot. Ennek oka a következő:

Mikor az operator+-t használtuk, akkor az operandusok változatlanok maradtak, és létrejött egy új objektum, mely az összeget tárolta el és teljesen független a két operandustól. Ezt az alábbi ábra vizualizálja, amin két Kurzus objektumot összeadva létrejön egy harmadik Kurzus:

operator+

Ha ezt k1 = k1 + k2; formában akarjuk használni, akkor az eredeti változó objektuma felülíródik az eredmény objektummal. Ezt a kövezkező két ábra szemlélteti.

operator+

operator+

Ilyen esetekben jobb az operator+= használata. Ennél már magában az operátorban benne van az értékadás, tehát az eredeti objektum módosul. Ezért is kell referenciával visszatérni, hiszen ugyanarról az objektumról beszélhetünk.

A hallgató hozzáadása is hasonlóan működik:

operator+

Fontos, hogy az eredeti Kurzus módosult, a Hallgato bekerült a kurzusra, de nem módosult. Ezért lehetett a paraméterben const &.

operator+

Mivel az operátorokat függvényeknek vettük, ezért a meghatározásoknál nemcsak az operátor neve, hanem a paraméter típusa is meghatározza. A Kurzus osztályban lehet két operator+=, az egyik Hallgato, a másik Kurzus paraméterrel. Az így kiegészített Kurzus osztály:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
#define KURZUS_LIMIT 10

class Kurzus {
  string nev, kod;
  unsigned felvette = 0;
  Hallgato hallgatok[KURZUS_LIMIT];
public:
  Kurzus(const string& nev, const string& kod) : nev(nev), kod(kod) {
  }

  Kurzus& operator+=(const Hallgato& h) {
    if ( felvette == KURZUS_LIMIT ) {
      cout << "A kurzus megtelt. Nem lehet felvenni." << endl;
    }
    else {
      hallgatok[felvette++] = h;
    }

    return *this;
  }

  Kurzus& operator+=(const Kurzus& k) {
    if ( k.felvette + this->felvette >= KURZUS_LIMIT ) {
      cout << "A két kurzus nem vonható össze. Nincs elég hely" << endl;
    }
    else {
      for ( unsigned i = 0; i < k.felvette; ++i ) {
        hallgatok[felvette++] = k.hallgatok[i];
      }
    }

    return *this;
  }
};

int main() {

  Kurzus k("Programozas II", "IB302G-1");
  Kurzus k2("Programozas II 2", "IB302G-13");

  Hallgato h1("Kreatív Név", "KNAA.SZE");
  Hallgato h2("Kreatívabb Név", "KBAA.SZE");

  k += h1;
  k2 += h2;
  k += k2;
}

A += operátor hasonlóan működik Kurzus-ra, mint Hallgató esetén:

operator+

Természetesen, a kurzushoz adott paraméter most sem módosul, így az lehet const &.

operator+

indexer operátor

Az indexer operátor különleges operátornak tekinthető. Szemantikáját általában tömbökhöz kötjük, elemek eléréséhez (de tetszőleges értelmezést adhatunk neki). Ehhez az operator[](int) metódust kell kiterjeszteni. Ha egy tömbben el akarunk érni egy elemet, azzal több dolgot is tehetünk. Egyszerűen lekérdezhetjük (módosítás nélkül):

1
cout << int_array[2];

Vagy az értéket módosíthatjuk:

1
int_array[2] = 55;

Mivel az első esetben csak az értékre vagyunk kíváncsiak, nem kell tudnunk módosítani azt. A második esetben már a visszaadott értéket szeretnénk módosítani, vagyis igazából az int_array objektumban lévő 2. indexen lévő elemet szeretnénk felülírni.

Azt már vettük, hogy ha egy függvény módosíthatja vagy sem az objektumot azt a const kulcsszóval jelezhetjük. Az első esetben nem módosíthatjuk tehát használjuk a const kulcsszót, a második esetben pedig nem.

Természetesen nem elég a metódust konstanssá tenni, hiszen ha a visszatérési értékben kiszivárog egy "kiskapu", akkor az objektumot elronthatjuk. Ennek kivédése érdekében a visszatérési értéket is máshogy kell megadnunk. Erről már szó volt a const-ról szóló részben. Az indexer operátorok kiterjesztése a Kurzus osztályra (annak éri meg, hiszen annak van tömbhöz köthető jellege):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#define KURZUS_LIMIT 10

class Kurzus {
  string nev, kod;
  unsigned felvette = 0;
  Hallgato hallgatok[KURZUS_LIMIT];
public:
  Kurzus(const string& nev, const string& kod) : nev(nev), kod(kod) {
  }

  //eset 1
  Hallgato& operator[](unsigned i) {
    if (i < felvette)
      return hallgatok[i];
    // valodi hibakezeles szukseges, ami majd kesobb jon
    cout << "HIBA: nincs " << i << " darab hallgato a kurzushoz rendelve" << endl;
    return hallgatok[0]; // nem szep, de ...
  }

  //eset 2
  const Hallgato& operator[](unsigned i) const {
    if (i < felvette)
      return hallgatok[i];
    // valodi hibakezeles szukseges, ami majd kesobb jon
    cout << "HIBA (const): nincs " << i<< " darab hallgato a kurzushoz rendelve" << endl;
    return hallgatok[0]; // nem szep, de ...
  }
};

Az első esetben egy referenciát adunk vissza, tehát ahova visszaadjuk, ott elérik az adott elemet pl. felül is írhatják az értékét. Mivel ezzel módosulhat a Kurzus, ez a függvény nem is const. A második esetben szintén referenciát adunk vissza, azonban ez most const & tehát nem írható felül az értéke. Ez biztosítja, hogy akármit is teszünk, a Kurzus nem fog megváltozni, így ez a függvény lehet const.

++ / -- operátorok

A ++a, a++, --a és a-- kifejezések mindenki számára ismerősek. A különbség a prefix és a postfix inkrementálás (dekrementálás) közt annyi pl. intekre nézve, hogy a prefix verzióban egy kifejezés kiértékelésekor már a megnövelt (csökkentett) érték van használva, míg postfix esetben a kifejezés kiértékeléskor még a nem megnövelt (csökkentett) értéket használjuk fel.

Mivel ezek is operátorok, ezeket is felül lehet definiálni. Azonban a pre és post verziók közt leírásban nem sok különbséget találunk, így valahogyan meg kell különböztetni azokat. Az operator kiterjesztése során a post verzióhoz egy plusz (nem használt) paramétert adunk, csupán a megkülönböztetés miatt.

Működésük miatt a visszatérési értékük sem teljesen egyezik meg. Nézzünk egy példát rá!

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <iostream>
class Kurzus {
  unsigned letszam = 0;

 public:
  Kurzus operator++(int) {  // a nem hasznalt parameter mutatja, hogy post incrementalast hajtunk vegre
    std::cout << "Udv a Kurzus post ++ operatorabol" << std::endl;
    Kurzus tmp = *this; // Mivel a post verzio eseteben a regi allapotot szoktuk visszaadni,
    // arrol kell egy mentest keszitenunk.
    letszam++;
    return tmp; // mivel egy lokalis valtozot adunk vissza, ami megszunik a metodus vegen
    // nem adhatunk vissza pl. referenciat.
  }

  Kurzus& operator++() {
      std::cout << "Udv a Kurzus pre ++ operatorabol" << std::endl;
      ++letszam;
      return *this; // mivel a pre verzioban a valtoztatott erteket adjuk vissza,
      // nem kell masolatot kesziteni es visszaadhatjuk az objektumot magat, hiszen
      // a mar frissitett adat kell nekunk.
  }
};

int main() {
    Kurzus k;
    ++k;
    k++;
}

Hasznos a kettőt együtt megvalósítani, és ekkor az egyiket a másik felhasználásával, hiszen ekkor hiba javításakor csak egy helyen kell módosítanunk.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include <iostream>
class Kurzus {
  unsigned letszam = 0;

 public:
  Kurzus operator++(int) {  // a nem hasznalt parameter mutatja, hogy post incrementalast hajtunk vegre
    Kurzus tmp = *this; // Mivel a post verzio eseteben a regi allapotot szoktuk visszaadni,
    // arrol kell egy mentest keszitenunk.
    /* Az alabbi resz ugyan az mint a pre verzioban. Hivjuk azt
    * Igaz, most csak egy sor, de ez akar tobb, osszetettebb kod is lehet.
    letszam++;
    */
    operator++();   // Ezzel a pre verziot meghivtuk, a noveles megtortent
    // az elozo ertek ugyan ugy lementesre kerult, a funkcionalitas megmaradt.
    return tmp; // mivel egy lokalis valtozot adunk vissza, ami megszunik a metodus vegen
    // nem adhatunk vissza pl. referenciat.
  }

  Kurzus& operator++() {
      ++letszam;
      return *this; // mivel a pre verzioban a valtoztatott erteket adjuk vissza,
      // nem kell masolatot kesziteni es visszaadhatjuk az objektumot magat, hiszen
      // a mar frissitett adat kell nekunk.
  }
};

int main() {
    Kurzus k;
    ++k;
    k++;
}

További operátor felüldefiniálás

Ahogy azt előzőekben láttuk, az operátor felüldefiniálás csak egy függvény / metódus megvalósítását jelenti. Ha egy osztálynak metódusként írunk meg egy operátort, akkor tudjuk, hogy az operátor első paraméterét elhagyhatjuk, hiszen az maga az objektum lesz. Természetesen, ha unáris (vagyis csak egy paramétert váró) operátorról van szó, akkor nem kell megadni semmit, hiszen csak az objektumra van szükség. Ilyen operátort pl. az operator++, operator--, operator! stb.

operator!

A felkijáltójel használata gyakran a negálást jelenti. Ez egy paramétert vár, magát az objektumot. A Kurzus példánál maradva nézhetjük, hogy bizonyos kurzusok jóváhagyásosak. Ezt egy bool értékkel le tudjuk tárolni, azonban ha meg akarjuk változtatni, akkor akár egy negálással is megtehetjük. Erre nézzünk példát!

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <iostream>

class Kurzus {
    // ...
    bool jovahagyasos = false;

  public:
    Kurzus() = default;

    bool isJovahagyasos() const { return jovahagyasos; }

    /**
    * A szamunkra fontos metodus / operator.
    * A metodus megcsereli a kurzus jovahagyasos jelleget.
    * Az uj jelleggel ter vissza bool formaban.
    */
    bool operator!() { // Fontos, hogy nem kell parameter, hiszen az egyetlen parameter az objektum lesz
        jovahagyasos = !jovahagyasos;
        return jovahagyasos;
    }
};

using namespace std;

int main() {
    Kurzus k;
    cout << "A kurzus jovahagyasos? " << k.isJovahagyasos() << endl;
    !k;
    cout << "A kurzus jovahagyasos? " << k.isJovahagyasos() << endl;
}

Konverziós operátorok

Sokszor konvertáljuk az adatokat és előfordulhat, hogy ezt egy általunk készített osztályra is meg kell tennünk. Ekkor írhatunk egy eljárást, ami mindig megcsinálja a konverziót, de kényelmesebb, ha meg tudjuk mondani, hogyan kell egy-egy adott konverziót megtenni.

Ha a Kurzus objektumunkat intté szeretnénk konvertálni, pl. a létszámmal legyen egyenlő a konverzió eredménye, akkor ezt megtehetjük egy konverziós operátorral.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
#include <iostream>
using namespace std;
class Kurzus {
    unsigned letszam = 0;
  public:
    Kurzus(unsigned letszam) : letszam(letszam) {}

    operator unsigned() const { return letszam; }
};

int main() {
    Kurzus k(8);
    cout << (unsigned)k << endl;
}

A fenti példán láthatjuk, hogy a kiíratásban unsigned típusra konvertáltuk a kurzusunkat. Ezt a Kurzusban definiált konverziós operátorral tettük meg. Fontos, hogy konverziós operátor esetében nem kell kiírni a visszatérési típust! Nem kell, hiszen a konverziós operátor neve egyértelűen meghatározza a visszatérési típust. Amire szeretnénk konvertálni, az lesz az eredmény. Eltérő típus esetében fordítási hibát kapunk.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
#include <iostream>
using namespace std;
class Kurzus {
    unsigned letszam = 0;
  public:
    Kurzus(unsigned letszam) : letszam(letszam) {}

    operator unsigned() const { return letszam; }
};

void foo(unsigned a) {
    cout << "Foo a: " << a << endl;
}

int main() {
    Kurzus k(8);
    foo(k);
}

Ebben a példában láthatjuk, hogy a konverziót nem kell explicit módon kiírnunk, automatikusan megtörténik. Ez azonban problémához is vezethet, hiszen lehet, hogy nem akartunk konvertálni, vagy éppen másik típusra akartuk volna. Erre megoldást az jelentene, ha a konverzió csakis az első módszerrel történhetne meg, vagyis csak akkor, ha kiírjuk (explicit módon). Ennek a megoldása, ha a konverziót ellátjuk az explicit taggal. Ekkor az automatikus konverziót letiltjuk, kötelezően ki kell írni a kívánt konverziót.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
#include <iostream>
using namespace std;
class Kurzus {
    unsigned letszam = 0;
  public:
    Kurzus(unsigned letszam) : letszam(letszam) {}

    explicit operator unsigned() const { return letszam; }
};

void foo(unsigned a) {
    cout << "Foo a: " << a << endl;
}

int main() {
    Kurzus k(8);
    cout << (unsigned)k << endl;
    // foo(k); forditasi hiba, hiszen itt implicit (nem kiirt) konverzio tortenik
    // es mivel megkoveteltuk a kiirast, ezt hibat okoz.
}

A konverzió másik iránya

Eddig azt néztük meg, hogyan lehet egy már létező objektumból konvertálással egy értéket előállítani. A másik irány, hogy egy értékből hogyan tudunk adott típusú objektumot előállítani. Erre példát a stringeknél láthattunk:

1
2
3
4
5
6
7
8
#include <iostream>
#include <string>
using namespace std;
int main() {
    string str = "Alma";
    // ebben az esetben az "Alma" const char* tipusrol konvertalunk std::string tipusra
    cout << str << endl;
}

Ezt a saját osztályunkra is megtehetjük. Mivel érték alapján kell létrehoznunk objektumot, a konstruktorhoz kapcsolódó részeket kell vizsgálnunk!

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
#include <iostream>
class Kurzus {
    unsigned letszam = 0;
  public:
    Kurzus() = default;
    Kurzus(unsigned letszam) : letszam(letszam) {}
    unsigned get_letszam() const { return letszam;}
};

using namespace std;

int main() {
    Kurzus k(5u);  // Fontos, hogy az unsigned jelzes nelkul, mar az int literal is konvertalodik
    Kurzus k2;
    Kurzus k3 = (unsigned)5;    // unsigned literal 5-os ertekkel Lehetne 5u is!

    cout << k.get_letszam() << endl
    << k2.get_letszam() << endl
    << k3.get_letszam() << endl;
}

Kimenet

5
0
5

Látszik, hogy az unsigned értékkel tudjuk a k3 objektumot inicializálni, hiszen létezik hozzá konstruktor. Terészetesen előfordulhat, hogy ezt az autmatikus konverziót (konstruktor hívást) szeretnénk elkerülni. Ekkor a konstruktornak is megadhatjuk, hogy explicit lehessen csak meghívni.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
#include <iostream>
class Kurzus {
    unsigned letszam = 0;
  public:
    Kurzus() = default;
    explicit Kurzus(unsigned letszam) : letszam(letszam) {}
    unsigned get_letszam() const { return letszam;}
};

using namespace std;

int main() {
    Kurzus k(5u);
    Kurzus k2;
    // Kurzus k3 = (unsigned)5;    // az explicit hivas miatt hibat kapunk
    Kurzus k4(5u);
    cout << k.get_letszam() << endl
    << k2.get_letszam() << endl
    // << k3.get_letszam() << endl
    << k4.get_letszam() << endl;
}

Utolsó frissítés: 2024-08-01
Létrehozva: 2019-09-23