1. Tudnivalók

Honlap, adminisztrációs portál

Az információ hiteles forrása: https://cpp11.eet.bme.hu/

Számonkérések jellege

  • Részvétel: laboron maximum 4 db hiányzás + 2 db csak online beadás
  • Nagy házi: 2 db egyénileg írt C++ program
  • Szorgalmik: jegybe beszámítanak!
  • (Vizsga): megajánlott jegy vagy vizsga

Nagy házi feladatok

  • 1. feladat: MyString osztály
    • Ki: 5. hét, Be: 8. hét
  • 2. feladat: Szabadon választott (minták)
    • Ki: 9. hét, Választás: 10. hét, Be: 13. hét
  • Pótlás: Pótlási héten

Pontszámok

  • Nagy házi: 2x5 pont (késve: max. 2x4 pont)
  • Szorgalmik: szinte minden héten, 1-2 pont/db, össz: ~15 pont
  • Vizsga: 40 pont

Megajánlott jegy pontszáma

Pontszám = ∑NHF + ∑szorgalmik

Megajánlott jegyként 4-est és 5-öst lehet szerezni. A megajánlott 4-est 15 ponttól, a megajánlott 5-öst 20 ponttól lehet megkapni. Ennek feltétele, hogy a nagy házi feladatok határidőre elkészüljenek, vagyis ne pótlásként legyenek beadva. (Ha megvan a két elfogadott nagyházi, akkor viszont javítani lehet több pontért, akár megajánlott jegyért.)

Vizsga

  • Vizsga pontszám: 40 pont
  • Nagyházi pontszám: 2x5

Összesen max. 50

Ponthatárok

  • –19: elégtelen
  • 20–31: elégséges
  • 32–38: közepes
  • 39–44: jó
  • 45–: jeles

Honlap, adminisztrációs portál

Az információ hiteles forrása: https://cpp11.eet.bme.hu/

Számonkérések jellege

  • Részvétel: laboron maximum 4 db hiányzás + 2 db csak online beadás
  • Nagy házi: 2 db egyénileg írt C++ program
  • Szorgalmik: jegybe beszámítanak!
  • (Vizsga): megajánlott jegy vagy vizsga

Nagy házi feladatok

  • 1. feladat: MyString osztály
    • Ki: 5. hét, Be: 8. hét
  • 2. feladat: Szabadon választott (minták)
    • Ki: 9. hét, Választás: 10. hét, Be: 13. hét
  • Pótlás: Pótlási héten

2. Típusok és osztályok

#include <stdio.h>

/* rational */
typedef struct Rat Rat;
struct Rat { int num, den; };

Rat make_rat(int n, int d);
Rat plus_rat(Rat r1, Rat r2);
void print_rat(Rat r);

int main(void) {
  Rat r1 = make_rat(1, 10);
  Rat r2 = make_rat(5, 10);
  Rat r3 = plus_rat(r1, r2);
  print_rat(r1);
  printf("+");
  print_rat(r2);
  printf("=");
  print_rat(r3);
}
#include <iostream>

class Rat { /* rational */
 public:
  Rat(int num, int den);
 private:
  int num_, den_;
};

Rat operator+(Rat r1, Rat r2);
std::ostream &operator<<
     (std::ostream &os, Rat r);

int main() {
  Rat r1(1, 10);
  Rat r2(5, 10);
  Rat r3 = r1 + r2;
  std::cout << r1 << '+' 
     << r2 << '=' << r3;
}

Tekintsük az alábbi két kódrészletet.

#include <stdio.h>

typedef struct Ratio Ratio;
struct Ratio {     /* rational number */
    int num;       /* numerator */
    int den;       /* denominator */
};

Ratio make_ratio(int num, int den) {
    /* Euclidean: gcd -> a */
    int a = num, b = den, t;
    while (b != 0)
        t = a%b, a = b, b = t;
    Ratio new = { num/a, den/a };
    return new;
}

Ratio plus_ratio(Ratio r1, Ratio r2) {
    return make_ratio(
        r1.num * r2.den + r2.num * r1.den,
        r1.den * r2.den);
}

void print_ratio(Ratio r) {
    printf("%d/%d", r.num, r.den);
}

int main(void) {
    Ratio r1 = make_ratio(1, 10);
    Ratio r2 = make_ratio(5, 10);
    Ratio r3 = plus_ratio(r1, r2);
    print_ratio(r1);
    printf("+");
    print_ratio(r2);
    printf("=");
    print_ratio(r3);
    return 0;
}
#include <iostream>

class Ratio {       /* rational number */
  public:
    Ratio(int num, int den);
    int num() const { return num_; }
    int den() const { return den_; }
  private:
    int num_;       /* numerator */
    int den_;       /* denominator */
};

Ratio::Ratio(int num, int den) {
    /* Euclidean: gcd -> a */
    int a = num, b = den, t;
    while (b != 0)
        t = a%b, a = b, b = t;
    num_ = num/a;
    den_ = den/a;
}

Ratio operator+(Ratio r1, Ratio r2) {
    return Ratio(
        r1.num() * r2.den() + r2.num() * r1.den(),
        r1.den() * r2.den());
}

std::ostream &operator<<(std::ostream &os, Ratio r) {
    os << r.num() << '/' << r.den();
    return os;
}

int main() {
    Ratio r1(1, 10);
    Ratio r2(5, 10);
    Ratio r3 = r1+r2;
    std::cout << r1 << '+' << r2 << '=' << r3;
}

Mi a különbség a kettő között? Legfeljebb a nyelv. Az első láthatóan C-ben van írva, egy C++ fordítóval le sem lehetne fordítani, mivel a make_ratio() függvény new lokális változójának neve C++-ban kulcsszó. A második kód C++, használ osztályokat, operátor átdefiniálást, konstruktort, és egyéb jellegzetes nyelvi elemeket is.

De ez csak a felszín. Mi a különbség tervezési szempontból közöttük? A válasz: semmi. A két program egymásnak tükörfordítása. Mindkét program objektumorientált (OO), még akkor is, ha a C változat egy nem objektumorientáltnak tartott nyelven íródott. Az objektumorientáltság nem arra utal, hogy milyen programozási nyelvvel dolgozunk, hanem arra, hogyan építjük fel a programunkat. Attól még, hogy nincsen a programban class kulcsszó, lehetnek benne osztályok.

Az osztály nem más fogalom, mint a típus. Mindkettő egy lehetséges értékkészlet és műveletek összessége. Egy típusba (vagy osztályba) sorolhatók a racionális számok, egy típusba (vagy osztályba) a sztringek, a képernyőn megjelenő ablakok és így tovább. Mindegyik értékek (1/2, 9/4, szavak, képernyőpozíciók, színek, feliratok) és hozzájuk tartozó műveletek (összeadás, konkatenálás, mozgatás) összessége.

A fenti C++ kódban tág értelemben véve nem csak a konstruktor, hanem az operator+ és az operator<< is részei az osztálynak. Hiszen a törtek összeadása ugyanúgy művelete ennek az osztálynak, mint a létrehozás. Nem volt rá szükségünk, hogy nyelvileg is tagfüggvényként definiáljuk azt (operator+ esetén ez a helyzet), vagy valamilyen szintaktikai okból egyáltalán nem tudtuk tagfüggvényként megadni (operator<< esetén pedig ez). A C kód Ratio struktúrája is egy osztály, a make_ratio(), plus_ratio(), print_ratio() pedig annak kvázi tagfüggvényei.

Tovább is mehetünk. Az stdio.h-ban definiált FILE típus is tekinthető osztálynak. Ugyan nyelvi eszköz nem fejezi ki, de az fopen(), fwrite(), fprintf() attól még ennek az osztálynak a tagfüggvényei. A nullával lezárt, sztringként használt, nyelvi szinten char*-gal reprezentált típust is nevezhetnénk osztálynak, lehetséges értékekkel: szavak, szövegek; és hozzájuk tartozó műveletekkel: strcat(), strlen(), strstr(). Az elvégezhető műveletek határozzák ezt meg, nem a szintaktika: nem csak a tagfüggvényeket kell néznünk, hanem az összes függvényt. Mindezt az angol szakirodalomban így nevezik: „the interface principle”.

A típus és az osztály tehát ugyanaz, ahogy a változó és az objektum is. (A C szabvány mindenhol objektumokról beszél változók helyett. Az objektum az ottani definíció szerint egy hely, ami adatot tárol.) A megkülönböztetést elméleti szempontból lényegtelen dolgok miatt szoktuk tenni. Egy C-ről szóló könyvben inkább típust mondunk, egy C++-ról szóló könyvben pedig csak akkor használjuk a típus szót, ha ki akarjuk fejezni, hogy esetleg szintaktikailag nem osztályról van szó. A C++ nyelv lehetővé teszi azt, hogy az adatok (tagváltozók) és függvények (metódusok) közötti összetartozást nyelvi szinten is kifejezzük, de a nyelvi eszköz hiánya nem kell megakadályozzon bennünket abban, hogy akár C-ben objektumorientált kódot írjunk. A nyelvi eszköz megléte csak annyit jelent, hogy a C++ szintaktikailag egyszerűbbé teszi az így szervezett program leírását, és segít betartani az OOP megszokott tervezési szabályait.

Apró megjegyzés azért kívánkozik a fentiekhez. Van, ahol az objektum mégsem ugyanaz, mint a változó. Például a referenciákat változóknak tekintjük, de nem objektumok. A temporális objektumok ugyan objektumok, de nincs nevük. Ezért nem szoktuk őket változóknak nevezni, és kijelölt helyük sincsen a memóriában.

3. A racionális szám osztály és az OOP

Mitől lesz az egész számpárból tört objektum? Bár egy racionális szám két egész szám hányadosa, ez nem jelenti azt, hogy a racionális szám ugyanaz, mint egy két egész számból álló számpár. A racionális szám több ennél, mivel az (1;2) számpár nem ugyanaz, mint a (2;4), viszont az 1/2 tört ugyanaz, mint a 2/4. Ha két egész számról azt mondjuk, az egyik egy tört számlálója, a másik pedig a nevezője, akkor egy teljesen új dolgot kapunk, ami több, mint a részek összege. Ezt hivatott kifejezni a Ratio osztály konstruktora és a make_ratio() függvény. Ennek a függvénynek a bemenete a két egész szám, és a kimenete pedig az előállított új objektum, a tört.

Amikor törtekkel műveleteket végzünk, rendszeresen közös nevezőre kell hozni a két törtet. Emiatt gyakran egyre nagyobb és nagyobb számokkal dolgozunk. Papíron érdemes is folyamatosan egyszerűsíteni. Egy programban pedig szinte muszáj, nehogy túlcsorduljanak az int változóink. Ennek az egyszerűsítésnek a legjobb helye a programban az a pont, ahol azt mondjuk, hogy „itt van két egész szám, mostantól kezdve ezek együtt egy törtet alkotnak”, tehát a konstruktor. Figyeljük meg, hogy ezzel a tervezési döntéssel – az egyszerűsítés konstruktorba helyezésével – garantáljuk azt, hogy a programunk összes törtje mindig egyszerűsítve lesz. Ez a kódunk egyetlen olyan függvénye, amely látja és írni is tudja a számlálót és a nevezőt, az a konstruktor. A C++-os, vagyis inkább az OOP-s gondolkodás hamarabb rávezet minket erre a helyes tervezési döntésre, hogy az egyszerűsítésnek a konstruktorban van a helye.

A C++ kódban a létrehozás után kapott tört adattagjait már meg sem tudjuk változtatni, és ez így van rendjén. Mivel az adattagok privátak, a nyelv biztosítja is, hogy ne férjünk hozzájuk sehol máshol a programban. C-ben csak a fegyelmünkön múlik, hogy betartjuk-e a szabályokat:

  1. Törtet csak a make_ratio()-val hozunk létre.
  2. Tört adattagjait mindig csak olvassuk, soha nem írjuk.

De amíg betartjuk ezeket, addig nincs gond. Mindezek az osztály használóira (OOP terminológiában: ügyfeleire) vonatkoznak. Az osztály tagfüggvényeire, vagyis azokra a kódrészletekre, amelyek közvetlenül hozzáférnek az osztály adattagjaihoz, egy egész más szabály érvényes. Mégpedig az, hogy biztosítaniuk kell, hogy a tört objektumban tárolt számláló és nevező relatív prímek, tehát a tört mindig egyszerűsítve van. Ez az osztály belső invariánsa.

Mitől lesz az egész számpárból tört objektum?

  • Racionális szám != két egész számból álló számpár.
    • (1;2) számpár != (2;4) számpár
    • de 1/2 tört = 2/4 tört

  • Tört: két egész szám = számláló és nevező.
  • Ezt fejezi ki a:
    • Rat osztály konstruktora.
    • make_rat() függvény.
  • Be: két egész szám, Ki: új objektum.

C99 újdonságok a C89-hez képest

C-ben struktúra inicializálásakor a tömbök inicializálásához hasonló szintaktikát alkalmazhatunk: kapcsos zárójelek között megadhatjuk az egyes adattagok értékét, ahogyan azt az alábbi sor is teszi. Ezt a C++ is megengedi, amennyiben nem definiálunk konstruktort vagy destruktort egy struktúrához (és persze nem próbáljuk new-nak elnevezni a változónkat).

Ratio new = { num/a, den/a };

Designated initializer

Tudjuk, hogy a fenti kódrészlet egy veszélyforrás a programban. Az adattagokra név szerint szoktunk hivatkozni, de ebben a nyelvi szerkezetben erre nincs lehetőség: itt az adattagok deklarációjának sorrendje számít. Ha megcseréljük a deklarációban egy sorrendet, esetleg felveszünk egy új adattagot, a program azonnal felborul, rossz esetben hibaüzenetet sem kapunk.

Ezt a hibalehetőséget kivédendő, bevezettek egy új szintaxist a C99-ben, amely lehetővé teszi, hogy az inicializálás helyén is név szerint hivatkozzunk rájuk. A szabvány ezeket designated initializer-eknek nevezi:

Ratio half = { .num = 1, .den = 2 };

Így olvashatóbb és robusztusabb is a kód; ha változtatjuk a struktúrát, nem kell végigböngésznünk a kódot, hol inicializáljuk rosszul. Ez a szintaktika megengedi azt is, hogy tetszőleges sorrendben adjuk meg az adattagokat, vagy csak némelyik adattagot adjuk meg. A többi olyankor a tömbökhöz hasonlóan nulla lesz:

/* így is 1/2 lesz */
Ratio half = { .den = 2, .num = 1 };

/* a nevező 1, a többi adattag 0 */
Ratio zero = { .den = 1 };

Apropó tömbök. Ez a szintaktika a tömböknél is működik, ott szögletes zárójelek között kell megadnunk azokat a (fordítási idejű konstans) indexeket, amelyeket inicializálni szeretnénk. A többi nulla lesz:

/* 1, 2, 0, 0, 0, 0, 0, ... */
int arr1[10] = { 1, 2 };

/* 0, 0, 5, 0, 3, 0, 0, ... */
int arr2[10] = { [2] = 5, [4] = 3 };

Compound literal

A C99-ben ennél tovább is mentek. A C nyelvben nincsenek konstruktorok, amelyeket függvényként használva objektumokat lehet létrehozni. Így bár egy struktúra változó létrehozásakor megadhattuk az értékeket, más kontextusban nem működött a struktúra, mint literális megadása:

/* ezzel minden rendben */
Ratio r1 = { 1, 2 };

/* ez viszont nem működik */
r1 = { 2, 3 };

/* és ez sem */
print_ratio({ 2, 3 });

Az egyetlen olyan összetett adattípus, amelynek megadásához külön szintaxis tartozott, a sztring volt C89-ben. Gondoljunk bele, a "hello" literális egy char [6] típusú értéket ad meg, ahogyan az 5 literális egy int típusút. Hogy ne kelljen az értékadást adattagonként elvégezni, vagy egy függvényhívás miatt változót létrehozni, a C99 megengedi azt, hogy egy cast operátorhoz hasonló szintaxissal megadjuk egy kapcsos zárójelpárról, hogy milyen összetett típusú értéket (compound literal) adunk meg vele:

/* ezzel minden rendben */
Ratio r1 = { 1, 2 };

/* C99-ben már lehet ilyen értékadást is */
r1 = (Ratio) { 2, 3 };

/* és bárhol névtelenül létrehozhatjuk */
print_ratio((Ratio) { 2, 3 });

Így az eredeti make_ratio() függvényünket így is írhatnánk:

Ratio make_ratio(int num, int den) {
    /* Euclidean: gcd -> a */
    int a = num, b = den, t;
    while (b != 0)
        t = a%b, a = b, b = t;
    return (Ratio) { .num = num/a, .den = den/a };
}

Ugyanez tömbökre is működik. Természetesen ott a típus megadásánál típus[]-t kell írnunk, nem típus*-ot, hiszen tömböt adunk meg, nem pointert. Például így:

#include <stdio.h>

void print_chars(char const *p) {
    while (*p != '\0') {
        putchar(*p);
        p++;
    }
    printf("\n");
}

void print_ints(int const *p) {
    while (*p != 0) {
        printf("%d ", *p);
        p++;
    }
    printf("\n");
}

int main(void) {
    print_chars( "hello" );
    print_ints( (int[]) { 2, 3, 4, 0 } );
}

Az így megadott objektumok automatikus memóriakezelésűek, ugyanaddig élnek, mint az ugyanabban a blokkban létrehozott változók. A fenti print_ints() függvényhívás tulajdonképp egy rövidítés ehhez:

int main(void) {
    /* ... */
    int __névtelen_tömb__[] = { 2, 3, 4, 0 };
    print_ints( __névtelen_tömb__ );
    /* ... */
}

Ne feledjük, ezek a nyelvi elemek csak a C-ben léteznek. De a C++-ban ott vannak a konstruktorok, amelyek többet tudnak ennél.

5. Static a C99 függvénydeklarációkban

A C++ számos nyelvi eszközt ad a kezünkbe ahhoz, hogy a saját típusainkat a beépített típusokhoz hasonló könnyedséggel kezelhessük. Az operator[] átdefiniálásának lehetősége, a másoló konstruktorok és a destruktorok például lehetővé teszik azt, hogy az std::vector osztály teljes értékű tömb típusként viselkedjen. A C nyelvben erre nincsen lehetőség, de a megfelelő nyelvi szerkezetekkel azért próbálják elérni azt, hogy kényelmes legyen az összetett típusok használata. A C99-ben a nyelvbe épített egyetlen tároló, a tömb lehetőségeit megpróbálták minél hatékonyabban kibővíteni. Térjünk most el a törtes példától, és nézzük meg az új nyelvi elemket!

A tömbök indexelését a C nyelv soha nem ellenőrzi. Ennek két oka van. Az egyik egyszerűen a teljesítmény: ha a program helyes, akkor sosem indexeli túl a tömbjeit, ezért felesleges futási időben ezzel bajlódni. A másik pedig az, hogy tömbműveleteknél legtöbbször nem is látjuk az eredeti tömb definícióját, hanem csak pointerekkel dolgozunk, amelyek a tömbbe mutatnak. Különösen igaz ez akkor, amikor tömböt függvénynek adunk át, ugyanis ott a paraméterátadás mindig indirekt. A függvények fejlécében megadott tömb típus alatt mindig pointert ért a nyelv, ezért az alábbi deklarációk teljesen egyenértékűek.

void func(char arr[100]);
void func(char arr[]);
void func(char *arr);

A híváskor így mindig csak a tömb kezdőcímét látja a függvény. Ez azonban nem jelenti azt, hogy a hívás helyén ne ismerhetnénk a tömb méretét, és bizonyos esetekben ne ellenőrizhetnénk azt. Gyakori eset, hogy egy függvény azért vár paraméterként egy tömböt, hogy az általa előállított eredményt oda tegye. A tömbnek ilyenkor általában van egy minimális elvárt mérete: például ha egy éééé-hh-nn formátumú dátumot szeretnénk bele írni, ez a méret 11. Az ilyen eseteket a static kulcsszóval jelölhetjük meg a fordító számára:

void date_tostring(Date d, char str[static 11]) {
    sprintf(str, "%04d-%02d-%02d", d.year, d.month, d.day);
}

char const *ratio_tostring(Ratio r, char str[static 24]) {
    sprintf(str, "%d/%d", r.num, r.den);
    return str;
}

Az így deklarált (fentebb definiált) függvényeknél a fordító, ha tudja, fordítási időben ellenőrzi, hogy

  • nem null pointert adunk-e át paraméterként,
  • hogy a hivatkozott tömbnek van-e annyi eleme, mint a megadott méret.

Ha ezt nem tudja megtenni (mert pl. pointert adunk, nem tömböt), a függvény használója számára akkor is hasznos ez, dokumentációként. Ne feledjük: ez C99 nyelvi elem, ilyen C++-ban nincs.

C-ben és C++-ban a függvények fejlécében megadott tömb mindig pointer, ezért az alábbi deklarációk egyenértékűek:

void func(char arr[100]);
void func(char arr[]);
void func(char *arr);

A minimális elvárt méret megadható C99-ben (a fordító, ha tudja, fordítási időben ellenőrzi):

C++-ban
nincs ilyen!
void date_tostring(Date d, char str[static 11]) {
    sprintf(str, "%04d-%02d-%02d", d.year, d.month, d.day);
}

char const *ratio_tostring(Ratio r, char str[static 24]) {
    sprintf(str, "%d/%d", r.num, r.den);
    return str;
}

6. C99 változó méretű tömbök (variable length array, VLA)

A tömböket gyakran ideiglenes tárolónak használjuk, és elég gyakori az is, hogy bár fordítási időben nem, de futási időben előre ismerjük a létrehozandó tömb méretét. A C99 megengedi azt, hogy a veremben létrehozott tömböknél futási időben kiértékelődő kifejezéssel adjuk meg a tömb méretét. Az ilyen tömböket a szabvány VLA-nak nevezi (variable length array). (Vigyázat: bár a lenti kódrészletet a legtöbb C++ fordító megérti és lefordítja, a C++ szabvány semelyik változata szerint sem elfogadott! A C++-ban ilyen célra egy std::vector<int>-et kell használni.)

int n;
printf("Hány szám lesz?\n");
scanf("%d", &n);        /* garantáltan futási időben :) */
int arr[n];

Az ehhez hasonló kódrészletek támogatása szinte elvárásunk lehet azután, hogy a C99-ben megengedték a változódefiníciók és az utasítások keverését (mixed declarations and code), vagyis hogy elengedték azt a megkötést, hogy az utasításblokkok tetején kell megadni a változókat. Az eszköz használatát senkinek nem kell bemutatni, mert tudjuk, ahol választhatunk a dinamikus (kézi) és az automatikus memóriakezelés között, ott mindig az utóbbit célszerű választani. Az így létrehozott tömbök egyébként teljesen ugyanúgy viselkednek, mint a konstanssal megadott méretű társaik; még a sizeof operátor is működik rajtuk. Azért sokat ne várjuk, az ilyen tömbök nem méreteződnek át mágikusan: minden VLA akkora marad megszűntéig, amekkora a létrehozása pillanatában volt.

for (int i = 0; i != 10; ++i) {
    int arr[rand() % 10 + 10];
    printf("Most %d eleme lett.\n", (int) (sizeof(arr)/sizeof(arr[0])));
}

A VLA-k mögött azonban több van, mint elsőre gondolnánk. Egy programozási nyelvnél nem „csak úgy” megy, hogy behozunk egy újfajta nyelvi elemet egy probléma megoldására és kész: gyakran helyette inkább olyan lehetőségeket kell a nyelvbe építeni, amiből már következik, hogy az áhított kódrészlet leírható.

Gondoljunk például a printf()-re. A Pascal writeln() eljárása tetszőlegesen sok paramétert kaphat, mindegyiket kiírja a kimenetre. A C printf()-je ugyanerre képes. Azonban a Pascal writeln() eljárása egy különleges, egyedi nyelvi elem; a programozó nem tud olyan saját függvényeket írni, amelyek akárhány paramétert kaphatnak. C-ben a printf() nem nyelvi elem, hanem egy szokványos függvény. Helyette a ...-tal jelölt, változó hosszúságú paraméterlista a nyelvi elem. Így C-ben tetszőlegesen írhatunk saját függvényeket is, amelyek bárhány paramétert kaphatnak.

Lássuk, miről van szó! A C89-ben semmilyen körülmények között nem lehetett leírni olyan kódot, amelyben egy típus mérete nem fordítási idejű konstans. Vagyis nem csak az int arr[n]; változódefiníció volt hibás, hanem helytelen volt a triviálisnak tűnő sizeof(int[n]) kifejezés is. A C99-ben pedig ezen változtattak. Az új verzió azt engedi meg, hogy egy lokális változó, tömb típus elemszámát egy kifejezés adja meg. Mivel egy ilyen típusnak a méretét már ki is tudja számolni, ezért megengedi azt is, hogy a veremben ilyen típusú változót hozzunk létre. Emiatt szabad leírnunk az alábbi sorokat:

int n;
scanf("%d", &n);

int *my_heap_arr = (int*) malloc(sizeof(int[n]));
int my_stack_arr[n];

A dolog onnantól válik izgalmassá, ha nem csak egy, hanem több dimenziós tömbökkel is dolgozunk. Ugyanis ezek összes dimenzióját megadhatjuk kifejezéssel. Ami önmagában megint triviálisnak hangzik, de a fordítónak meg kell oldania azt, hogy akárhány változó méretű dimenzió szerint lehessen indexelni. Ehhez pedig az kellett, hogy olyan pointert is lehessen létrehozni, amelynek típusa kifejezések értékeitől függ. Mert ha egy 20×10-es tömb egyes soraira mutató pointer „10 elemű tömbökre mutató pointer” típusú, akkor egy m×n-es tömb soraira mutató pointer „n elemű tömbökre mutató pointer” kell legyen:

int arr1[20][10];
int (*parr1)[10];
parr1 = arr1;
int arr2[m][n];
int (*parr2)[n];
parr2 = arr2;

A méretinformációra mindenképpen szükség van a pointer típusában, mert egy cím számításakor (pointer aritmetika), azaz pl. a parr1+1 kifejezés kiértékelésekor tudnia kell a fordítónak, hány bájtot kell ugrani a memóriában. A parr1+1 kifejezésnél sizeof(int[10])-et, mivel *parr1 típusa int[10]; a parr2+1 esetén pedig sizeof(int[n])-t, mivel *parr2 típusa int[n].

Egy VLA-ra mutató pointer azért hasznos, mert így egy ilyen tömböt át tudunk adni függvénynek is paraméterként. Ehhez csak egy megfelelő függvénydefiníció kell, amelynél a függvény fejlécében jelezzük azt, hogyan függ a paraméterektől a pointer típusa. Az egyedüli korlátozás, hogy a méretet megadó paramétert előbb kell szerepeltetni, mint a pointert. Mire a pointer deklarációjával találkozik a fordító, addigra a méretet megadó kifejezés változói már ismertek kell legyenek:

/* deklaráció */
void print(int, int, char (*)[*]);

/* definíció */
void print(int height, int width, char (*arr)[width]) {
    for (int y = 0; y != height; ++y) {
        for (int x = 0; x != width; ++x)
            putchar(arr[y][x]);
        putchar('\n');
    }
}

A függvény definíciója a formális paraméterek neveit is tartalmazza. Ha csak deklarációt adunk meg, a nevek szerepeltetése nem kötelező; a kifejezéssel megadott pointer típusú paramétereknél is elhagyható a név. Mivel azonban a deklarációban char(*)[]-ot nem írhatunk (definiálatlan méretű tömb továbbra sem létezik!), valahogy jelölni kell azt, hogy a méret is meg lesz adva. Ezt a deklarációban a tömb mérete helyére beírt * karakterrel tudjuk megtenni. Vagyis a fenti függvénydeklaráció valami ilyesmit jelent: „A függvény vár két int típusú értéket és egy pointert. A pointer char[] tömbökre mutat, amelyek méretét a függvény majd valahonnan tudni fogja.” Hogy honnan tudja, az pedig már a függvény dolga, és azt a definíció adja meg.

Talán mégis a legjobb, ha inkább a deklarációkba is beírjuk a változók nevét, és kifejezzük azt, hogy mi mitől függ, hiszen ez a függvény használóit segíti. De ne feledjük, a tömbök helyett mindig az első elemükre mutató pointerek adódnak át paraméterként! Ezért az összes alábbi deklaráció egyenértékű:

void print(int height, int width, char arr[height][width]);
void print(int height, int width, char arr[*][width]);
void print(int height, int width, char arr[*][*]);
void print(int height, int width, char (*arr)[width]);
void print(int height, int width, char (*arr)[*]);
void print(int, int, char (*)[*]);

Az így átadott tömbnek egyébként nem kell feltétlenül VLA-nak lennie, hanem lehet konstans méretű tömb is. A C99 nyelv a változóval megadott típusú pointer által képessé vált arra is, hogy tetszőleges méretű (szélességű és magasságú) kétdimenziós tömböt adjon át függvényparaméterként.

#include <stdio.h>
#include <stdlib.h>

void print(int height, int width, char arr[height][width]) {
    for (int y = 0; y != height; ++y) {
        for (int x = 0; x != width; ++x)
            putchar(arr[y][x]);
        putchar('\n');
    }
}

int main(void) {
    char man[4][3] = {
        { ' ', 'o', '/' },
        { '/', '|', ' ' },
        { ' ', '|', ' ' },
        { '/', ' ', '\\' },
    };
    char diamond[2][2] = {
        { '/', '\\' },
        { '\\', '/' },
    };

    print(4, 3, man);
    printf("\n");
    print(2, 2, diamond);
}
A C++ szabvány semelyik
változata sem támogatja!
Van helyette std::vector.
int n;
printf("Hány szám lesz?\n");
scanf("%d", &n); /* garantáltan */
int arr[n]; /* futási időben :) */

A stack-en jön létre, még a sizeof operátor is működik rajta:

for (int i = 0; i != 10; ++i) {
    int arr[rand() % 10 + 10];
    printf("Most %d eleme lett.\n", (int) ( sizeof(arr) /
	    sizeof(arr[0]) ));
}

7. A C99 flexibilis tömb adattag nyelvi eleme

A C-ben gyakran dinamikus memóriakezelés használatára kényszerülünk, és ez lassíthatja a programot nem csak a foglaláskor, hanem az adatok elérése közben is. Tekintsük az alábbi két kódrészletet:

Statikus eset

typedef struct MyArrayStat {
    size_t siz;
    double data[200];
} MyArrayStat;

/* foglalás */
MyArrayStat ma;




/* használat */
ma.data[12] = 37.3;

Dinamikus eset

typedef struct MyDynArr {
    size_t siz;
    double *data;
} MyDynArr;

/* foglalás */
MyDynArr *ma;
ma = malloc(sizeof(MyDynArr));
ma->data = malloc(sizeof(double[200]));
ma->siz = 200;

/* használat */
ma->data[12] = 37.3;

Nézzük, mi történik az objektumok foglalásakor!

Statikus eset

  1. Az ma változó létrehozásakor a stack pointer („meddig foglalt a verem”) arrébb állítódik MyDynArr struktúra méretével.

Dinamikus eset

  1. Az ma pointer létrehozásakor a stack pointer arrébb állítódik egy pointernyivel.
  2. Az első malloc() hívás elkezdi vizsgálni a foglalt/szabad területek nyilvántartását. Talál egy megfelelő méretű üres helyet. Bejelöli foglaltnak, frissíti az adatszerkezetet. Visszatér a címével.
  3. Ezt beírjuk az ma pointerbe.
  4. A második malloc() hívás újból nekiáll helyet keresni. Ha megvan a hely, bejelöli foglaltnak, frissíti az adatszerkezetet.
  5. A cím bekerül az ma->data pointerbe.

Ez csak egyszeri költség, a tömb létrehozásakor. Viszont mi történik egy elem elérésekor, pl. a fenti esetben, amikor a 37.3 bemásolódik a tömbbe? Ez érdekesebb, hiszen ez nem egyszeri költség, hanem a tömb minden egyes használatakor megtörténhet.

Statikus eset

  1. Az ma változó valahol a veremben van. A címéhez hozzáadódik annyi bájt, ahányadik bájttól a struktúrában a data[] tömb kezdődik. Ez még fordítási időben elvégezhető.
  2. Ezután jön egy címszámítás: az előző címhez hozzáadódik index*sizeof(double).
  3. Az így kapott helyre beíródik a 37.3.

Dinamikus eset

  1. Kiolvasódik a memóriából az ma pointer értéke.
  2. A statikus esethez hasonlóan kiszámolódik ehhez képest a data adattag címe, de csak futási időben.
  3. Ezután jön egy újabb memóriaolvasási művelet, mivel nem tudjuk, hogy a double[] tömb hol van. Annak címét a data pointer tárolja.
  4. A pointer kiolvasása után jön a címszámítás, index*sizeof(double).
  5. És végül a kapott című helyre íródik az érték.

Ez az indirekció költsége. A pointer változó értékét ki kell olvasni, mielőtt a címszámítást el tudjuk végezni, és ez egy plusz memóriaművelet lehet minden alkalommal. Ha magát a struktúrát is egy indirekción keresztül érjük csak el (esetleg dinamikusan van foglalva), akkor még rosszabb a helyzet.

A C99 bevezetett egy új nyelvi eszközt, amellyel egyszerűbb esetekben az indirekciók és a dinamikus memóriafoglalások száma csökkenthető. A flexible array member egy olyan tömb egy struktúra utolsó adattagjaként, amelynek a mérete nem definiált:

typedef struct MyArrayFlex {
    size_t siz;
    double data[];
} MyArrayFlex;

Egy ilyen struktúrát nincs értelme a veremben foglalni. A struktúra méretébe a flexibilis tömb nem számít bele. A flexibilis tömb adattaggal azt jelezzük a fordítónak, hogy ott majd egy tömb lesz; a létrehozásáért mi felelünk. A fordító ilyenkor a tömb helyét meg tudja határozni, tehát indexelhetjük is a tömböt, csak létre kell hozni azt valahogyan. Ezt egy megfelelően összerakott malloc() hívással tehetjük meg, amelyben több helyet kérünk, mint amekkora a struktúra üresen lenne:

MyArrayFlex *ma;

ma = (MyArrayFlex*) malloc(sizeof(MyArrayFlex) + sizeof(double[200]));
ma->siz = 200;
ma->data[12] = 37.3;

Az így létrehozott tömbök több okból is gyorsabbak, mint az előbb bemutatott párjuk:

  1. Gyorsabb foglalás és felszabadítás: két malloc() és két free() helyett csak egy-egy van.
  2. Gyorsabb adatelérés: a tömb hivatkozásakor eggyel kevesebb indirekció, eggyel kevesebb memóriaművelet.

Mivel egy speciális helyzetről van szó, sok korlátozás is van ennél a nyelvi eszköznél. A megkötések nagy része abból adódik, hogy a fordítónak el kell tudnia végezni a címszámítást a flexibilis tömbön:

  1. Csak a struktúra végén lehet ilyen tömb. (Különben az utána lévő adattagokat nem lehetne elhelyezni.)
  2. Csak egy ilyen tömb lehet benne. (Különben nem lehetne tudni, melyik hol kezdődik.)
  3. Ilyen tömböt tartalmazó struktúrát nem lehet másik struktúrába vagy tömbbe tenni.
  4. Dinamikusan kell foglalnunk a struktúrát, különben nem tudjuk megadni a tömb méretét.

Csak C99-ben van ilyen, C++-ban nincs.

Statikus eset

typedef struct MyArrayStat { size_t siz; double data[200]; } MyArrayStat;

MyArrayStat ma; /* foglalás */
ma.data[12] = 37.3; /* használat */

Dinamikus eset

typedef struct MyDynArr { size_t siz; double *data; } MyDynArr;

MyDynArr *ma; /* foglalás */
ma = malloc(sizeof(MyDynArr));
ma->data = malloc(sizeof(double[200]));
ma->siz = 200;

ma->data[12] = 37.3; /* használat */

C++ apróságok

9. Tagváltozók inicializálása

class Example {
    double a = -2.0; // C++11
    double b = 0.0;
public:
    Example() : a(0), b{ -2 } { // C++11, Osztályhierarchiák ea
        a = 5;
        b = -5.0;
    }
private:
    static double c;
    inline static double d = 33.2; // C++17
};

double Example::c = 0;

Sorrend: a = -2.0; => a(0) => a = 5;

Az újabb C++ szabványok kényelmesebbé teszik a tagváltozók inicializálását.

class Example {
    double a = -2.0; // C++11
    double b = 0.0;
public:
    Example() : a(0), b{ -2 } { // C++11, Osztályhierarchiák ea
        a = 5;
        b = -5.0;
    }
private:
    static double c;
    inline static double d = 33.2; // C++17
};

double Example::c = 0;

Immár a C-ben megszokott változóinicializálási módon (double a = -2.0;), közvetlenül a definíció helyén adhatunk kezdőértéket a tagváltozóknak. Objektumok esetén ez a konstruktoruk meghívását jelenti. Így sok esetben szükségtelenné válik, hogy konstruktort írjunk az osztálynak. Ez a megadási módszer persze csak szintaktikai cukor, valójában az inicializálást a (z adott esetben automatikusan létrejövő) konstruktorok végzik.

Természetesen a megszokott inicailizációs módszereket továbbra is használhatjuk: a konstruktor inicailizálólistájával történő (a(0)) és a konstruktor törzsében értékadással (a = 5;) történő kezdőérték adást. Ha ezek közül többet is megvalósítunk, akkor az a(0) felülírja a double a = -2.0;-t, és az a = 5; mindkettőt felülírja.

A kerek zárójel helyett kapcsos zárójelet is használhatunk az inicailizálólistában (és máshol is). Ennek okáról az Osztályhierarchiák előadásban lesz szó.

A statikus tagváltozók definíciója elég speciális. Mivel ezek a változók tulajdonképpen az osztály névterébe helyezett globális változók, hagyományosan valamelyik forrásmodulban (.cpp fájl) kell ezeket definiálnunk. Nem lehet a definíció fejlécben, mert akkor több modulba is beszerkesztenénk, ami linkelési hibát okozna, ill. nem lehet az osztályban sem. Ez sokszor kényelmetlen, ezért a C++17 szabvány bevezette az inline statikus tagváltozót. Ebben az esetben a fordítóprogram hozza létre a statikus változó definícióját az általa kiválasztott modulban. A módszer előnye, amellett, hogy egyszerűbb a programozónak, hogy a teljes osztályt megvalósíthatjuk a fejlécben.

10. Literálisok

int main() {
    int a = 26, b = 032, c = 0x1A;
    int d = 0b11010;                // C++14
    double e = 1'000'000.000'001;   // C++14
    int f = 1'2'3;
}

Az aposztrof előtt és után számjegy kell álljon.

Sem C-ben, sem a C++ korábbi verzióiban nem volt lehetőség bináris számliterális megadására, pedig sokszor hiányzott ez a lehetőség, a C++14 ezt is lehetővé teszi. A hexadecimális literális (0x) mintájára 0b előtaggal kell megadni.

A literálisok olvashatóságát nagymértékben javítja, hogy a C++14-ben bevezetett szabály alapján aposztrofokat helyezhetünk el a literálisban. A fordító az aposztrofokat figyelmen kívül hagyja, mintha ott sem lennének. Az aposztrofokat úgy kell elhelyezni, hogy előtte és mögötte is számjegy legyen (hexa literális esetében az A...F betűket is számjegynek tekintjük). Tehát előjel után vagy tizedespont mellé nem tehetjük, és két aposztrofot sem tehetünk egymás mellé. Az aposztrofok szokásos elhelyezése, hogy 3-3 számjegyet választanak el, de a C++ nem tesz ilyen megkötést.

int main() {
    int a = 26, b = 032, c = 0x1A;
    int d = 0b11010;                // C++14
    double e = 1'000'000.000'001;   // C++14
    int f = 1'2'3;
}

C++20/23 újdonságok

12. Modulok - fejléc helyett

Az interfész fájl: általában: pelda.cppm, MS: pelda.ixx

export module pelda;

import std; // C++23
import <iostream>; // ha csak C++20 van
export import masikmodul;

int negyzet(int a) { return a*a; } // nem exportált
export int kob(int a) { return a*a*a; }

export class Example {
public:
    void hello() const{ std::cout << "Hello" << std::endl; }
};

export {
    class valami {...};
    void fv() {...}
}

A továbbiakban a C++20 és C++23 néhány újdonságáról lesz szó. Ezeket az elemeket egyelőre nem építettük be a tárgy fővonalába.

A fejlécfájlok a C nyelv létrehozásától kezdve elválaszthatatlan részét képezték a C és C++ nyelvnek, és a kezdetektől nem sokban változtak. A preprocesszor segítségével kerülnek beépítésre a fordítás során. Céljuk, hogy interfészt biztosítsanak a különválasztott programrészek és a felhasználási helyük között. Sok probléma van velük, pl. a többszörös beszerkesztés elkerülése, a fejlécfájlok sorrendje, vagy az, hogy sokszor akár több tízezer kódsorból állnak, amivel a fordítónak minden fordításkor meg kell birkóznia.

A C++20-ban bevezetett modulok sok problémát kiküszöbölnek.

  • A modulok importjának sorrendje tetszőleges.
  • A modulok lefordítása külön történik, így importálásukkor nem kell újrafordítani ezeket, így a fordítás sokkal gyorsabb lehet.
  • A makrók hatása modulon belül marad, nem rondítanak bele más fejlécfájlba vagy a .cpp fájlba.
  • A using utasítás is aggodalom nélkül használható a modulinterfész fájlban.
  • Nem szükséges szétválasztani a deklarációt és az implementációt, ahogy a fejlécfájl és a hozzá tartozó cpp esetében történt.

További részletek itt

Az interfész fájl kiterjesztése általában .cppm, a Visual Studio azinban jelenleg .ixx kiterjesztést ír elő. A következő példa egy modulinterfész fájlt mutat be:

export module pelda;

import std; // C++23
import <iostream>; // ha csak C++20 van
export import masikmodul;

int negyzet(int a) { return a*a; } // nem exportált
export int kob(int a) { return a*a*a; }

export class Example {
public:
    void hello() const{ std::cout << "Hello" << std::endl; }
};

export {
    class valami {...};
    void fv() {...}
}
  • export module pelda;: ez a pelda nevű modul.
  • import std;: Az összes szabványos modult importálja, nem kell egyenként importálni pl. az iostream-et, a string-et, exception-t, stb. Csak a C++23 szabványtól él.
  • import <iostream>;: C++20 szabványú fordítóval egyenként importálhatjuk a modulokat.
  • export import masikmodul;: ha lenne egy masikmodul nevű modul, és szeretnénk azt használni a pelda modulban, akkor importáljuk, de azt is szeretnénk, hogy a masikmodul tartalma a pelda modult beépítő számára is elérhető legyen, és ne kelljen külön importálnia, akkor használjuk az export import szerkezetet.
  • A negyzet függvény a pelda modult beépítő számára nem elérhető, mert nem exportáljuk. A kob függvény viszont kívülről is elérhető.
  • Mindent exportálhatunk, aminek neve van, pl. függvényt, class-t, változót. Csoportosan is exportálhatjuk, ha export csoportba tesszük.

13. Modulok - használat

main.cpp

import pelda;
import std;

using namespace std;

int main() {
   int k = kob(8);
   Example x;
   fv();
   println("8^3 = {}", k);
}

A #include helyett az import utasítást használjuk. Mivel ezt nem a preprocesszor dolgoza fel, van pontosvessző a végén. A main.cpp:

import pelda;
import std;

using namespace std;

int main() {
   int k = kob(8);
   Example x;
   fv();
   println("8^3 = {}", k);
}

14. Modulok - implementációs fájl

pelda.cpp

module pelda; // nincs export kulcsszó

using namespace std; // az import std;-t örökli az interfészből

void Example::valami() const {
    ...
}

Mindent megvalósíthatunk az interfész fájlban, de ha szeretnénk, használhatunk külön implementációs fájlt is. Itt nem kell külön importálni az interfészt, ez implicite megtörténik. Az interfészben importált modulok az implementációban is használhatók. Az inline és template függvényeket az interfészben implementáljuk. pelda.cpp:

module pelda; // nincs export kulcsszó

using namespace std; // az import std;-t örökli az interfészből

void Example::valami() const {
    ...
}

15. print(), println()

    printf("%11.6f", d); // C

    std::cout << std::fixed << std::setw(11) 
        << std::setprecision(6) << d; // C++

    print("{:11.6f}", d); // C++23

    int n { 75 };
    std::println("<{:4}>", n);       /* <  75>        */
    std::println("<{:{}}>", n, 6);   /* <    75>      */
    std::println("<{1:{0}}>", 6, n); /* <    75>      */

    std::vector<int> v { 16, 32, 64 };

    std::println("{}", v);              // [16, 32, 64]

    std::println("{:n}", v);            // 16, 32, 64

Egyedi formázás saját típussal: std::formatter template class specializációja.

A C++-ban bevezetett stream kiírás előnye, hogy saját típussal is működik, ha a >> operátort megírjuk hozzá. Hátránya, hogy sokkal nehézkesebb, mint a C-s printf. A C++23-ban bevezetett kiírás függvények ezt a problémát megoldják. A print és println függvénysablonok saját típussal is működnek. Egyedi formázást is készíthetünk a std::formatter template class specializációjával. A print/printlnfüggvényekkel fájlba is írhatunk. Az újfajta kiírás a példában bemutatott dolgoknál sokkal többet tud.

    printf("%11.6f", d); // C

    std::cout << std::fixed << std::setw(11) 
        << std::setprecision(6) << d; // C++

    print("{:11.6f}", d); // C++23

    int n { 75 };
    std::println("<{:4}>", n);       /* <  75>        */
    std::println("<{:{}}>", n, 6);   /* <    75>      */
    std::println("<{1:{0}}>", 6, n); /* <    75>      */

    std::vector<int> v { 16, 32, 64 };

    std::println("{}", v);              // [16, 32, 64]

    std::println("{:n}", v);            // 16, 32, 64

16. Az űrhajó operátor

int main() {
    double a = -0.0;
    double b = 0.0;
 
    auto eredmeny = a <=> b; // a háromutas (three-way) has. op.
 
    if (eredmeny < 0)
        std::println("a<b");
    else if (eredmeny > 0)
        std::println("a>b");
    else if (eredmeny == 0)
        std::println("a és b ekvivalens");
    else
        std::println("a és b sorrendje nem megállapítható");
}

A C++20-ban vezették be a háromutas összehasonlító operátort. Előnye pl. sztringek esetében: a sztringeket elég egyszer összehasonlítani, aztán csak az egész típusú eredményt kell többször vizsgálni, ami sokkal gyorsabb. (C-ben az strcmp, ill. a qsort és bsearch hasonlító függvényének eredménye volt hasonló.)

int main() {
    double a = -0.0;
    double b = 0.0;
 
    auto eredmeny = a <=> b; // a háromutas (three-way) has. op.
 
    if (eredmeny < 0)
        std::println("a<b");
    else if (eredmeny > 0)
        std::println("a>b");
    else if (eredmeny == 0)
        std::println("a és b ekvivalens");
    else
        std::println("a és b sorrendje nem megállapítható");
}

17. Az eredmény típusa

Egész típusok: strong_ordering

  • strong_ordering::less: a<b
  • strong_ordering::greater: a>b
  • strong_ordering::equal: a==b

Valós típusok: partial_ordering

  • partial_ordering::less: a<b
  • partial_ordering::greater: a>b
  • partial_ordering::equivalent: a és b ekvivalens
  • partial_ordering::unordered: az egyik NaN

Saját típusnál ez is használható: weak_ordering

  • weak_ordering::less: a<b
  • weak_ordering::greater: a>b
  • weak_ordering::equivalent: a és b ekvivalens

Egész típusok: strong_ordering

  • strong_ordering::less: a<b
  • strong_ordering::greater: a>b
  • strong_ordering::equal: a==b

Egész típusoknál az értékek sorrendje mindig egyértelműen megállapítható, ezért ha a spaceship operátort egészekre használjuk, a visszaadott típus strong_ordering.

Valós típusok: partial_ordering

  • partial_ordering::less: a<b
  • partial_ordering::greater: a>b
  • partial_ordering::equivalent: a és b ekvivalens
  • partial_ordering::unordered: az egyik NaN

Valós típusoknál az értékek sorrendje nem mindig egyértelmű. A -0.0 és a 0.0 nem egyenlő egymással, ha a tárolt biteket hasonlítjuk össze, de mégis ugyanazt az értéket jelentik, ezért ekvivalensek. Ha egy művelet eredménye NaN (Not a Number, pl. 0-val osztás), akkor az semmivel sem egyenlő, még egy másik NaN-nal sem. Ha az összehasonlított értékek között van NaN, akkor az eredmény unordered.

Saját típusnál ez is használható: weak_ordering

  • weak_ordering::less: a<b
  • weak_ordering::greater: a>b
  • weak_ordering::equivalent: a és b ekvivalens

Lehet olyan adattípus, ahol a sorrend mindig megállapítható, de az egyenlőség mellett ekvivalencia is lehetséges, ekkor használható a weak_ordering. Például, ha sztringeket hasonlítunk össze, és a kis- és nagybetűket nem különböztetjük meg egymástól, akkor két sztring lehet nem egyenlő de ekvivalens. ("Hello"=="hello").

18. Összehasonlító operátorok túlterhelése

class MyType {
    ...
    [[nodiscard]] bool operator==(int) const;
};

Ez mind működni fog:

    MyType a { ... };
    if (a == 6) { println("a == 6"); }
    if (6 == a) { println("6 == a"); }
    if (a != 6) { println("a != 6"); }
    if (6 != a) { println("6 != a"); }

Ha még ezt is betesszük:

[[nodiscard]] std::strong_ordering operator<=>(int) const;

Akkor minden összehasonlítás (<, <=, >, >=) működni fog.

A C++20-tól kezdve elég sokkal kevesebb összehasonlító operátort megírnunk. Az összehasnlító operátorokat innentől kezdve két csoportba soroljuk: alap (==, <=>) és származtatott (!=, <, <=, >, >=).

Ha a két alap operátort megvalósítjuk, a fordító az összes többit létrehozza nekünk.
class MyType {
    ...
    [[nodiscard]] bool operator==(int) const;
};

Ez mind működni fog:

    MyType a { ... };
    if (a == 6) { println("a == 6"); }
    if (6 == a) { println("6 == a"); }
    if (a != 6) { println("a != 6"); }
    if (6 != a) { println("6 != a"); }

Ha még ezt is betesszük:

[[nodiscard]] std::strong_ordering operator<=>(int) const;

Akkor minden összehasonlítás (<, <=, >, >=) működni fog, bármelyik oldalon is legyen az int.

Igazából elég lenne az űrhajó operátort megvalósítani, de sok esetben hatékonyabb, ha az ==-t is megvalósítjuk.

A [[nodiscard]] jelentése, hogy ha ezt a függvényt hívjuk, de a függvény visszatérési értékét nem használnánk fel, akkor a fordító ezt nem fogja megengedni.

19. Default összehasonlító operátorok

Az első C++ szabvány óta = op. magától létrejön: a tagváltozókat másolja, ha nem írjuk meg. A C++20-tól kezdve ugyanúgy létrejön az == op (és a <=>).

Minden tagváltozónak kell legyen <=> vagy (== és <) operátora.

Ha van saját összehasonlító op., a default nem jön létre, de előírhatjuk:

class Typ {
    ...
 [[nodiscard]] bool operator==(int) const;
 [[nodiscard]] std::strong_ordering operator<=>(int) const;

 [[nodiscard]] auto operator<=>(const Typ&) const = default;
 [[nodiscard]] bool operator==(const Typ&) const = default;
};

Ahogy az első C++ szabvány óta = operátor magától létrejön, és a tagváltozókat másolja át, ha nem írjuk meg, ugyanúgy létrejön az == (és a <=>) operátor a C++20-tól kezdve.

Minden tagváltozónak kell legyen <=> vagy (== és <) operátora. Ha nincs, nem lesz automatikus hasonlító operátor.

Ha mi megvalósítjuk valamelyik összehasonlító operátort, akár más típusra, a default nem jön létre, de előírhatjuk:

class Typ {
    ...
 [[nodiscard]] bool operator==(int) const;
 [[nodiscard]] std::strong_ordering operator<=>(int) const;

 [[nodiscard]] auto operator<=>(const Typ&) const = default;
 [[nodiscard]] bool operator==(const Typ&) const = default;
};