Erős típusok használata

Czirkos Zoltán, Pohl László · 2023.08.23.

Egy erősen típusos nyelvben a típusok és a függvénynév túlterhelések segítségével egyszerűbben, célratörőbben, biztonságosabban fogalmazhatjuk meg a mondanivalónkat. Az egyes részfeladatainkat, megkötéseinket is új típusokkal modellezhetjük. Ez az írás néhány példát mutat meg, hogyan lehet ezeket az eszközöket használni.

Gyengén vagy erősen típusos a nyelv?

A típusokat az egyes programozási nyelvek eltérően kezelik. Ezek alapján a nyelveket gyakran nevezzük erősen típusosnak (strongly typed) vagy gyengén típusosnak (weakly typed).

A két fogalmat elég pongyolán szokás használni. Egyes helyeken a C és C++ nyelveket erősen típusosnak szokták nevezni, mert minden változónak előre meg kell határozni a típusát, összemosva így a fogalmat a statikus és a dinamikus típusossággal. Máskor gyengén típusosnak nevezik őket, mondván, hogy az egyes beépített típusok között nagyon sok az automatikus, akár adatvesztéses konverzió: az intchar és a doubleint például ilyenek.

Sose gondoljuk azt, hogy a gyengén vagy erősen típusos nyelvek jobbak a másiknál. Hogy melyik előnyösebb, az mindig a felhasználási területen múlik, a konkrét feladaton. Inkább tekintsünk egy nyelv típusosságának „mértékére” úgy, mint egyfajta kompromisszumra. Az erősen típusos nyelvek szigorúak: a változókat típus szerint deklarálni kell, a konverziókat külön kiírni – ez hosszabb, részletesebb programszöveget eredményez. Többet gépelünk, cserébe több ellenőrzést kapunk. A gyengén típusos nyelvek programkódjai általában rövidek, némelyek az érthetőség határáig tömörítettek; a kódot olvasva nagyon kell ismernünk a nyelvet, hogy rájöjjünk, mi történik. Cserébe ezekben sokkal természetesebb például a generikus programozás.

Erős típusok – új megközelítésben

A C++ a típusokat, konverziókat szigorúbban kezeli, mint a C. Ugyanakkor lehetőséget biztosít a függvénynevek és az operátorok átdefiniálásán (overload) keresztül arra, hogy tömören fejezzük ki magunkat a kódban. Közben a statikusan ismert típusok miatt mégis minden lépésünk fordítási időben ellenőrzött lehet.

A típusokat okosan használó programban minden különálló feladatot egy típusra (osztályra) bízunk, Ez nem akadályozza, hanem kifejezetten segíti és biztonságosabbá teszi a munkát. Legközismertebb példa erre az std::string, még ha nem is gondoltunk rá eddig ilyen szempontból. Mennyivel egyszerűbb azt használni, mint egy char*-ot!

A sztring ötletét tovább is lehet vinni. Képzeljük el, hogy egy nevet el kell tárolnunk a programban. Hogyan tesszük ezt? Első gondolatunk egy sima std::string lenne. Azonban kiköthetjük, hogy a név nem lehet üres sztring. Kérdés ezek után, hogy egy ilyen függvény esetén kinek a dolga ellenőrzni, üres-e a kapott sztring?

std::string get_name();

Bújhatjuk a kommenteket és a dokumentációt... Ha ezt a sztringet több függvényen keresztül adogatjuk tovább, előbb-utóbb a programunk tele lesz if (name != "") vizsgálatokkal. Ennél sokkal jobb ötlet külön típussal modellezni a problémát:

class Name {
  private:
    std::string name;
  public:
    explicit Name(std::string name) : name(name) {
        if (this->name == "")
            throw std::invalid_argument("a name cannot be empty");
    }
};

Mit válaszolunk ezek után arra a kérdésre, hogy térhet vissza a következő függvény üres sztringgel? Nyilvánvalóan lehetetlen, hogy ilyen történjen!

Name get_name();

A technikát erős típusoknak (strong types) nevezik – ez nem keverendő a nyelv fentebb említett erősen típusos (strongly typed) voltával. Ez az írás a technika használatára mutat példákat. Megint elő fog jönni pár C++11 nyelvi elem. De ne csak ezekre koncentráljunk, hanem a módszerekre, ötletekre is!

1. Az automatikusan inicializálódó Int

„C++-ban objektum nem jön létre anélkül, hogy ne lenne inicializálva.” Igaz ez? Osztálynál igen, de beépített típusnál nem. A beépített int, char, double stb. típusú változók ugyanúgy viselkednek, mint C-ben: ha nem adunk nekik kezdeti értéket, inicializálatlanok maradnak, és ez hibalehetőség a programban.

De nem csak hibalehetőség, hanem programozási kényelmetlenséget is jelenthet. Tegyük fel, hogy van egy osztályunk, sok adattaggal. Az osztály alapértelmezett konstruktorában minden adattag alapértelmezett konstruktorát szeretnénk hívni. Mi a helyzet, ha van egy int adattagunk? Akkor amiatt az egyetlen adattag miatt régen konstruktort kellett írnunk, sok int adattagnál pedig mind fel kellett sorolnunk azokat. Ha csak egy kimarad, máris gond van. A C++11-ben odaírhatjuk melléjük, hogy = 0, de ez továbbra is kifelejthető.

Mi lenne, ha nem int-eket használnánk? Hanem helyettük valami mást, ami magától is nullára inicializálódik, ugyanakkor bárhol használható, ahol egy normál int is. Mint például az alábbi osztály. Ennek alapértelmezett konstruktorába a fordító beépíti az i_ = 0 inicializálást; így egy Int objektum külön kérés nélkül is nullára inicializálódik:

C++11
class Int {
  private:
    int i_ = 0;
  public:
    Int() = default;
    Int(int i) : i_{i} {}
    operator int() const { return i_; }
};

A konstruktornak azonban van még egy szerepe. Egyparaméterű konstruktor lévén ez egy konverziós lehetőséget is ad. Ezeket úgy tekinti a fordító, mintha azt mondanánk: így kell int-ből Int-et csinálni. És ezt használja is, ha egy kifejezést csak így tud értelmezni. A konverzió teszi lehetővé az alábbi sorok leírását:

Int i = 5;      /* Int i = Int{5}; */
i = 6;          /* i = Int{6}; */

Az operator int() függvény pedig egy konverziós operátort ad meg: éppen a fordított irányt. Emiatt a fordító elfogadja az alábbi sorokat is:

Int i = 5;
std::cout << i + 3;             /* static_cast<int>(i) + 3 */
std::cout << i;                 /* static_cast<int>(i) */
double *arr = new double[i];    /* new double[static_cast<int>(i)]; */

Mindez azért működik, mert a fordító előbb a kifejezéseket nyelvtanilag elemzi (operátorok, precedenciák), és utána nézi meg azt, hogyan helyettesíthetők be a konkrét értékek. Ha pedig a típusok nem stimmelnek, akkor még nem adja fel, megpróbálkozik konverziókkal is. Az i+3 kifejezésnél előbb megnézi, talál-e Int+int operátort, de nincs. Utána azt próbálja meg, hogy a létező operátorok (int+int, double+double, ...) közül nem lenne-e használható valamelyik konverzióval. Így talál rá az (Int→int)+int lehetőségre: ha a bal oldali operandusból int-et csinál, akkor az int+int jó lesz. Ezért nem kell saját kiíró operátort sem írni: az std::cout<<i kifejezés értelmezése is megoldható a konverzióval. Akár mindkét irány használható egyszerre:

Int i = 5;
i = 4 + i;      /* i = Int{4 + static_cast<int>(i)}; */

Fontos: sose felejtsük el, hogy az ilyen jellegű kódolásnak nincsen futási idejű költsége! A sok inline függvény miatt a fordító szinte teljesen kioptimalizálja az egész varázslást, és vissza fog cserélni mindent egy ahhoz hasonló, vagy akár ugyanolyan kódra, mintha sima int-ekkel írtunk volna mindent.

Az Int objektum címe?

Ha azt szeretnénk, hogy egy Int objektumnak még a címe is int* legyen, semmi más dolgunk nincs, mint átdefiniálni az addressof operátort: operator&. Nem konstans Int objektumra int* mutat, konstans Int-re int const *, ezért:

class Int {
  public:
    /* ... */
    int * operator&() { return &i_; }
    int const * operator&() const { return &i_; }
};

int main() {
    Int i;
    int *pi = &i;
    *pi = 4;
    std::cout << i;
}

Az addressof operátorra gondolhatunk úgy is, mint az alapértelmezett konstruktorhoz hasonlóan automatikusan megírt tagfüggvényre. Ha nem mondunk mást, a függvény ennyit csinál: return this. Hivatalosan viszont nem számít ún. special member function-nek, mint mondjuk az alapértelmezett konstruktor.

Az explicit kulcsszó

Az automatikus, felhasználó által definiálható konverziók nagyban növelik a nyelv használhatóságát. A saját típusainkat beilleszthetjük a beépített típusok közé: bármikor sztring objektummá válhat egy char const *, bármikor komplex számmá egy double, és így tovább. Vigyázni kell azonban arra, hogy ne történjen szándékolatlan konverzió. Nem minden konstruktor számít értelmes konverziónak. Hogy melyik igen, azt viszont a fordító nem tudja eldönteni! Példának definiáljunk egy dinamikus tömb osztályt, amelynek konstruktora a méretet adja meg:

class DynArray {
  public:
    DynArray(size_t siz);
};

Formailag ez semmiben nem különbözik az Int osztály konstruktorától, ezért ha kell, ezt is konverzióra fogja használni a fordító. Emiatt viszont az alábbi, értelmetlen programrészletek lefordulnak:

DynArray a = 5;     /* DynArray a = DynArray(5); */

void func(DynArray const &arr);
func(6);            /* func(DynArray(6)); */

Azért értelmetlenek ezek, mert a DynArray(size_t) nem jelenti azt, hogy „így kell egész számból tömböt csinálni”. Erre való az explicit kulcsszó, amely az automatikus konverziót akadályozza meg. Ha egyparaméterű konstruktort írunk, mindig gondolkozzunk el rajta, hogy az konverziót jelent-e, és ha nem, írjuk elé az explicit szót:

class DynArray {
  public:
    explicit DynArray(size_t siz);
};

C++11 óta az explicit kulcsszó több paraméterű konstruktoroknál is használható lehet. Az alábbi kódot akkor fogadja el a fordító, ha T-nek van (int, int) paraméterű, nem explicit konstruktora. Ha a konstruktor explicit, akkor csak a return T(2, 3) engedélyezett.

T get_t() {
    return {2, 3};
}

Hasonló a helyzet a konverziós operátoroknál is. Gyakran írunk például az osztályoknak operator bool() konverziót, hogy lehetővé tegyük az if (obj) alakú utasításokat. Például egy hálózati kapcsolat osztálynál:

class NetworkConnection {
  public:
    /* ... */
    operator bool() const { return is_open(); }
};

NetworkConnection conn1{/* ... */};
if (conn1)
    /* ... */;

Ez különösen veszélyes, mert a C-ből örökölten a logikai típus aritmetikai típusnak is számít. Emiatt lefordulnak az alábbi kódrészletek is:

NetworkConnection conn1{/* ... */}, conn2{/* ... */};

conn1 + 19;         /* static_cast<bool>(conn1) + 19 */
conn1 >= conn2;     /* static_cast<bool>(conn1) >= static_cast<bool>(conn2) */

A C++11 a konverziós operátorokra is bevezette az explicit kulcsszót, hogy ilyesmi ne történhessen:

C++11
class NetworkConnection {
  public:
    /* ... */
    explicit operator bool() const { return is_open(); }
};

NetworkConnection conn1{/* ... */}, conn2{/* ... */};

conn1 + conn2, conn1 >= conn2;      /* fordítási hiba */
if (conn1)                          /* rendben, az if () speciális, mert bool-t vár */
    /* ... */;
if (conn1 && conn2)                 /* rendben, a && operátor speciális */
    /* ... */;

A fenti osztályhoz hasonlóan csinálhatunk 0.0 értékűre inicializálódó Double-t, false alapértékű Bool-t, vagy akár nullptr értékűre inicializálódó pointereket. Ha kiírjuk ezen típusok paraméter nélküli inicializálását, akkor az említett nullaszerű értékek éppen ezen típusok alapértékei is:

bool b1;        /* inicializálatlan */
bool b2{};      /* false */
char *p1;       /* inicializálatlan */
char *p2{};     /* nullptr */

A C-s örökség miatt ezek a típusok mind inicializálhatóak a 0 értékkel is. Ezt hagyjuk meg a C programoknak: a bool b=0 C++-ban inkább képzavarnak tűnik.

Az automatikusan inicializálódó beépített típus emiatt sablon osztályként is megírható. A T{} formájú inicializálás használható az inicializáló listán és alapértelmezett paraméterértékként is, így az alábbi két osztálysablon egyformán jól megoldja a feladatot. Ezek bármelyikéből példányosíthatunk okosított int-et, double-t vagy különféle pointereket:

template <typename T>
class InitBuiltin {
  private:
    T val_;
  public:
    InitBuiltin(): val_{} {}
    InitBuiltin(T val): val_{val} {}
    operator T() const { return val_; }
};
template <typename T>
class InitBuiltin {
  private:
    T val_;
  public:
    InitBuiltin(T val = T{}): val_{val} {}

    operator T() const { return val_; }
};

A fenti sablonból létrehozott osztályoknak rövid neveket is adhatunk. A C++11-ben a typedef helyett a típusok átnevezésére a using kulcsszót illik használni, mert ez tisztább, és támogatja a template-eket is (a typedef ilyet nem tudott). A szintaktikája egyszerű, using újnév = réginév:

C++11
using Int = InitBuiltin<int>;
using Bool = InitBuiltin<bool>;
template <typename T> using InitPtr = InitBuiltin<T*>;

Bool b;             /* == false */
InitPtr<int> p;     /* == nullptr; */

A using kulcsszavas típusdefiníció olvashatóbb is, mint a typedef-es párja, mert segít, hogy a definiált típus és a név ne keveredjenek szintaktikailag. A C-s operátorok hol bal, hol jobb oldalon állnak, így a keveredés gyakori. Gondoljunk csak a függvényekre mutató pointerekre:

typedef double (*FuncPtr)(double);      /* C++98 */
using FuncPtr = double (*)(double);     /* C++11 */

Az osztályok alapértelmezett konstruktora képes kezdeti értéket beállítani, de a beépített típusok inicializálatlanok kezdeti érték nélkül.

int helyett Int osztály => külön kérés nélkül is 0-ra inicializálódik:

C++11
class Int {
  private:
    int i_ = 0;
  public:
    Int() = default;
    Int(int i) : i_{i} {}
    operator int() const { return i_; }
};

Az egyparaméteres konstruktor konverziós lehetőséget is ad. Így tud a fordító int-ből Int-et csinálni:

Int i = 5;      /* Int i = Int{5}; */
i = 6;          /* i = Int{6}; */

2. RAII = Resource Acquisition is Initialization

Tekintsük az alábbi kódrészletet:

void do_something() {
    SomeObj *x = new SomeObj();
    /* ... */
    some_function();
    /* ... */
    delete x;
}

Mi ezzel a kódrészlettel a probléma? Az, hogy memóriaszivárgás léphet fel. Ha kivételt dob a közepén meghívott függvény, az x által mutatott objektum nem lesz felszabadítva. Első körben ezt úgy is javíthatjuk, ha elkapjuk a kivételt:

void do_something() {
    SomeObj *x = new SomeObj();
    /* ... */
    try {
        some_function();
    }
    catch (...) {   /* elkapjuk, bármi is */
        delete x;
        throw;      /* továbbdobjuk */
    }
    /* ... */
    delete x;
}

Ezt azonban könnyű kifelejteni. Hogy rájöjjünk, hova kell ilyen try...catch blokkokat tenni, ahhoz át kell néznünk a kódot. A függvényekre pedig nincs ráírva, hogy dobhatnak-e kivételeket, vagy nem. Talán csak a dokumentációjukból derül ki.

Egyes programozási nyelvek a try ... catch szerkezetnél megengednek egy harmadik, finally blokkot. A finally ezeken a nyelveken egy olyan kódrészletet ad meg, amelyet le kell futtatni akkor is, ha volt kivétel, és akkor is, ha nem volt:

NEM C++
void do_something() {
    SomeObj *x = new SomeObj();
    /* ... */
    try {
        some_function();
    }
    finally {       NEM C++
        delete x;
    }
}

A C++-ban viszont ilyen nincsen. Valószínű nem is lesz, mert a C++-nak egyszerűen nem ez a logikája. A finally szerkezetet támogató nyelvek programjai általában menedzselt környezetben (managed environment) futnak, és a dinamikusan foglalt objektumok felszabadításáért a futtató környezet felel (szemétgyűjtés, garbage collection). Ezeknél lehet ugyan tudni, hogy az objektumok valamikor fel lesznek szabadítva, de azt nem, hogy mikor. Ezekben a nyelvekben nem szokott lenni destruktor sem, hiszen ha nem tudjuk, hogy mikor fut le, akkor nincs értelme feladatot bízni rá.

Nem így a C++-ban. A C++-ban a veremben létrehozott objektumok azonnal megszűnnek, ahogy az őket tartalmazó utasításblokkot elhagyja a végrehajtás. Emiatt van értelme a destruktoroknak is. És itt a lényeg: mivel tudjuk, mikor fognak lefutni a destruktorok, kritikus feladatok is bízhatók rájuk, mint például egy memóriaterület felszabadítása.

Vegyük észre, hogy a destruktorok általánosabb szerepet tudnak betölteni, mint az automatikus szemétgyűjtés! A szemétgyűjtő csak a dinamikusan foglalt memóriaterületeket képes felszabadítani, a destruktorok azonban bármilyen erőforrásért felelhetnek, legyen az dinamikus memória, nyitott hálózati kapcsolat, a képernyőn lévő bezárandó ablak vagy grafikus kártyára feltöltött textúra. Bár az automatikus szemétgyűjtés kényelmes, a lehetőségei korlátozottak.

A fenti probléma úgy oldható meg nagyon könnyen, ha létrehozunk egy objektumot, amelynek destruktora végzi el a feladatot. Például egy ehhez hasonló okos pointer osztályt:

class SmartPtr {
  private:
    SomeObj *x_;
  public:
    explicit SmartPtr(SomeObj *x) : x_{x} {}
    ~SmartPtr() { delete x; }    // !
    SmartPtr(SmartPtr const &) = delete;
    SmartPtr & operator=(SmartPtr const &) = delete;
    SomeObj& operator*() const { return *x_; }    // !
};

void do_something() {
    SmartPtr x(new SomeObj());
    /* ... */
    some_function();    /* ha kivételt dob, itt ~x */
    /* ... */
} /* ha nem dobott kivételt, akkor itt ~x */

Így biztosan fel lesz szabadítva az objektum, akár dob kivételt a hívott függvény, akár nem. Nem lehet elrontani, mert az x objektum destruktorát a fordító automatikusan meghívja. Kivétel esetén a verem visszafejtése (stack unwinding) közben, normál esetben pedig a függvényből visszatéréskor. Ezt az elvet RAII-nek nevezzük.

Resource Acquisition is Initialization

Ha bármilyen erőforrást foglalunk le, bízzuk azt azonnal egy automatikus memóriakezelésű objektumra! Így annak az objektumnak a destruktora biztosan el fogja végezni az erőforrás felszabadítását.

A RAII (Resource Acquisition is Initialization) név arra utal, hogy egy objektum inicializálásához kell kötni az erőforrás megszerzését – bár láthatóan az elv inkább a destruktorokról szól. Lehetne ezt SBRM-nek is nevezni (Scope Based Resource Management), de inkább a RAII név terjedt el.

A RAII elvet nem csak a függvények belsejében foglalt erőforrásoknál érdemes alkalmazni, hanem osztályok adattagjainál is. Képzeljünk el egy osztályt, amelynek egy pointere egy dinamikusan foglalt memóriaterületre mutat. Ha ezt a mutatót egy sima, „nyers” pointerben tároljuk, destruktort kell írnunk. Ha egy okos pointer hivatkozik rá, a destruktorral nem kell foglalkoznunk, mert az adattag megszűnésekor az okos pointer intézkedni fog. Minden automatikus:

class MyClass {
  private:
    SomeObj *x;
  public:
    /* ... */
    MyClass()
        : x(new SomeObj()) {
    }
    ~MyClass() {
        delete x;
    }
};
class MyClass {
  private:
    std::unique_ptr<SomeObj> x;
  public:
    /* ... */
    MyClass()
        : x(new SomeObj()) {
    }

    /* nem kell destruktor */

};

A fenti SomeObjPtr osztály a C++11-es std::unique_ptr-hez hasonló. Okos pointerekről később részletesebben lesz szó.

A fenti kódról eszünkbe juthat a C++98-as std::auto_ptr osztály us. De azzal sok gond volt, elavultnak számít, nem használjuk már.

void do_something() {
    SomeObj *x = new SomeObj();
    some_function(); // mem.szivárgás léphet fel, ha kivételt dob
    delete x;
}

1. megoldás: Kapjuk el a kivételt:

Nehézkes
void do_something() {
    SomeObj *x = new SomeObj();
    try { // hová tegyük?
        some_function();  // dob kivételt?
    }
    catch (...) {   /* elkapjuk, bármi is */
        delete x;
        throw;      /* továbbdobjuk */
    }
    delete x;
}

3. Any – type erasure

A C++-ban többféle generikusság is létezik. A sablonok lehetővé teszik azt, hogy egy osztály vagy függvény forráskódjában egy típust fordítási időben kicseréljünk: így létre tudjuk hozni egész számok, valós számok, valamilyen objektumok listáját. A heterogén kollekciókban valamilyen ősosztálybeli objektumokat (pontosabban azok pointereit) tároljuk, és ott a különböző típusú objektumok futási időben cserélhetőek.

Időnként előfordul az, hogy egy olyan változót szeretnénk létrehozni, amelyik „tényleg generikus”. Mit jelent ez? Egy ilyen változó tetszőleges típusú értéket fölvehet, legyen az akár beépített, akár felhasználó által definiált. Továbbá képes arra, hogy dinamikusan, futási időben típust váltson. Olyasmi kellene legyen ez a változó, mint a C-s void* pointer, ami bármire mutathat. Itt is a programozónak kell majd tudnia, hogy épp milyen típusú értéket tárol a változó, de szerencsére ez a C-s verziónál okosabban megcsinálható, futási idejű ellenőrzésekkel, és érték szerinti másolással.

Gondoljuk meg, mire lenne itt szükségünk!

int main() {
    Any a;
    
    a.set(5);
    std::cout << a.get<int>();
    
    a.set(std::string("hello"));
    std::cout << a.get<std::string>();
}

A változó itt egyszer int, máskor std::string típusú értéket tárolt el. A .set() tagfüggvény sablonfüggvény kell legyen, hogy tetszőleges típusú paramétert át tudjon venni. Igazából ugyanúgy sablon, mint a .get(), ahol a programozó adja meg a típust. Csak az előbbinek van paramétere, amiből a fordító a sablonparamétert le tudja vezetni. A .get()-nél erre nincsen lehetőség, mivel annak nincs paramétere.

A .set() függvénynek adott objektumok eltérő méretűek lehetnek, mégis ugyanannak az Any típusú, előre lefoglalt memóriaterülettel rendelkező változónak kell eltárolnia őket. Ez csak úgy oldható meg, ha a tényleges tárolás az Any objektumon kívül történik. Ezért dinamikus memóriakezelést kell használnunk: az így lefoglalt objektumnak az Any csak a memóriacímét fogja tárolni. Ahányféle objektumot kap a .set(), annyiféle objektumot kellhet tárolni; így ha a .set() sablonfüggvény, akkor a dinamikusan létrehozott objektumnak is sablonnak kell lennie.

A new int és a new std::string kifejezésektől kapott int* és std::string* típusú pointerek nem kompatibilisek egymással. Az Any tárolhatna void*-ot, de jobban járunk, ha az eltárolandó objektumot becsomagoljuk egy segédobjektumba, a segédobjetumok számára pedig létrehozunk egy közös ősosztályt. Tulajdonképpen egy egyelemű heterogén „kollekciót” kapunk (ahol a kollekció nem véletlenül van idézőjelben, már csak a tervezési mintára való utalásként szerepel):

class Any {
    class ContainerBase {                    // Üres ősosztály
      public:
        virtual ~ContainerBase() = default;
    };

    template <typename T>
    class Container final : public ContainerBase { // Container<T>::data tárol
      public:
        T data;
        Container(T const &what) : data(what) {}
    };

    ContainerBase* pdata = nullptr;          // Emiatt kell az ősosztály
};

A módszer neve: type erasure. Ebben a közös ősosztály „csak azért van, hogy legyen”, mert kell a közös ős.

A .set() és a .get() függvények ezután már könnyen megvalósíthatóak. A .set() megadott típusú csomagoló objektumot foglal, és beállítja a pointert. Ezen a ponton elveszik a típusinformáció, de azt a .get() újból megkapja a sablonparaméterében, és egy dynamic_cast visszaalakítja a segédobjektum referenciáját a megfelelő típusúra:

template <typename T>
void Any::set(T const& what) {
    ContainerBase* newpdata = new Container<T>(what);
    delete pdata;
    pdata = newpdata;
}

template <typename T>
T& Any::get() {
    return dynamic_cast<Container<T>&>(*pdata).data;
}

A dynamic_cast futási idejű hibaellenőrzését itt ki is lehet használni. Ha helytelen a konverzió, std::bad_cast típusú hibát fog dobni (referenciák esetén így viselkedik). A dynamic_cast-hoz szükség van virtuális függvényre, de ez a feltétel szerencsére automatikusan teljesül az ősosztálybeli virtuális destruktor miatt, amire amúgy is szükségünk lenne.

Már csak egy dolgot kell megoldanunk, az Any objektumok érték szerinti másolását. A ContainerBase* indirekció miatt a fordító által generált másoló konstruktor helytelenül működik, csak a pointert másolja, nem az objektumot. A pointer által mutatott objektum dinamikus típusa azonban ismeretlen, ezért a szokásos trükköt kell alkalmaznunk: a virtuális konstruktort. Ez egy újabb virtuális függvény a segédobjektum számára. Szerencsére a .clone()-t most csak egyszer kell implementálnunk, a sablon osztályban:

class Any {
  private:
    class ContainerBase {
        virtual ContainerBase* clone() const = 0;
    };
    
    template <typename T>
    class Container final : public ContainerBase {
        ContainerBase* clone() const override { return new Container<T>(*this); }
    };

    ContainerBase* pdata;
};

template <typename T>
Any::Any(Any const& to_copy) {
    pdata = to_copy.pdata->clone();
}

A teljes változat letölthető innen: any.cpp. A C++17 már tartalmaz egy ehhez hasonlóan működő osztályt, std::any néven.

Szeretnénk egy változót, ami bármilyen értéket felvehet:

int main() {
    Any a;
    
    a.set(5); // sablonfüggvény (template)
    std::cout << a.get<int>(); // sablonfüggvény (template)
    
    a.set(std::string("hello"));
    std::cout << a.get<std::string>();
}

Any: bármilyen (akár saját) típusú változót tud tárolni => csak a címét + dinamikus memória.

set: sablonfüggvény => a dinamikusan létrehozott objektum sablon lesz.

int* és std::string* típusú pointerek nem kompatibilisek egymással => lehetne void*, de előnyösebb, ha segédobjektum, közös őssel.

4. Mennyiség = mérőszám és mértékegység

Sokszor ha fizikai mennyiségeket szeretnénk a programban kifejezni, egyszerűen valós számokat használunk. Ez azonban elég könnyen hibákhoz vezethet. Ha azt írjuk, double ido = 1.2 és hogy double hossz = 1.7, a hossz/ido kifejezés értelmes, és egy sebességet ad. A hossz+ido kifejezés viszont értelmetlen. De csak nekünk az, sajnos a fordítónak nem, mert az csak annyit lát ebből, hogy össze szeretnénk adni két valós számot. A mértékegységeket már eldobtuk.

Gyakori probléma az is a mérnökségben, hogy eltérő mértékegységrendszereket használunk. Hosszakat ki lehet fejezni méterben, hüvelykben vagy mérföldben is. Az átváltásokra külön kell figyelni, és tudjuk, hogy nem egyszer adódott már ebből probléma (lásd a Gimli Glider és a Mars Climate Orbiter történetét). Ráadásul ez nem csak mértékegységekkel rendelkező, hanem mértékegység nélküli számoknál is gond. A szögeket mértékegység nélküli számmal mérjük, és mégis eltérő „mértékegységekkel” is kifejezzük őket néha: fokban és radiánban. Mindenki beleütközött már abba a problémába, hogy a sin() függvény (vagy épp a zsebszámológépe) radiánt vár fok helyett.

Az alábbi programban, kihasználva az erősen típusos C++ adta lehetőségeket, egy sablon osztály segítségével a mérőszámokhoz (magnitude) mértékegységeket (multitude, unit) társítunk. A mennyiség (quantity) objektumok egy egyszerű valós szám formájában tartalmazzák a mérőszámokat, a mértékegységet pedig a típusuk mutatja meg, méghozzá SI (MKS: méter, kilogramm, másodperc) mértékegységrendszerben. Mivel így minden mennyiség eltérő típusként jelenik meg a programban, függvényeket adhatunk meg hozzájuk, amelyeket a fordító automatikusan tud kezelni. Így nemcsak hogy nem lehet majd eltérő típusú mértékeket összeadni, de szorzások és osztások esetén a fordító ki fogja találni a keletkező mennyiség mértékegységét!

A mértékegységrendszerek közötti átváltásokhoz pedig literális operátorokat használunk, amely a C++11 nyelvi újdonságai közül az egyik. A végeredmény az, hogy ilyesmiket írhatunk a programban:

Length s = 56.8_km;
Time t = 1.2_h;
Speed v = s/t;

Mindeközben a fizikát a fordító intézi a háttérben. Mindennek futási idejű költsége nulla, mivel a sablonok körüli összes munkát a fordító fordítási időben elvégzi. A lefordított program ugyanúgy néz ki, mintha nyers double számokkal írtuk volna meg, de az összes átváltás és ellenőrzés automatikus!

A mennyiség osztály

Itt is arra a következtetésre juthatunk, hogy egy sablon osztályt kell létrehoznunk a mennyiségek számára. Minden mennyiség hasonlóan működik (pl. az összeadás, kivonás műveletek mindegyiknél egyformák), azonban a célunk az, hogy a különféle mértékegységekkel (típusokkal) rendelkező osztályokon ne lehessen elvégezni ezeket a műveleteket.

A különféle mértékegységek az egyes alapmértékegységekből építhetők fel. Ezek hatványozhatóak: m, m2, m3 a hossz, terület, térfogat mértékegységei; de negatív hatványok is léteznek: m-1 jelentése: méterenként, s-1 jelentése: másodpercenként. Különféle alapmértékegységek is keverhetők, pl. m1s-1: méter másodpercenként (sebesség), vagy kg1m-3: kilogramm köbméterenként (sűrűség). Mint látjuk, ezek mind leírhatóak három egész számmal, a m, kg és s alapmértékegységek hatványkitevőivel. Ezért ez a három szám lesz a sablonparaméter:

template <int M, int KG, int S>
class Quantity {
  public:
    double magnitude;
    explicit Quantity(double magnitude): magnitude{magnitude} {}
};

Az alábbiakban sokszor fogjuk leírni a template <int M, int KG, int S> kódrészletet. Ez azt mutatja, hogy a három alapmértékegység hatványait egy külön osztállyal kellene összefogni, amely egyetlen template paramétere lehetne a Quantity osztálynak. Az egyszerűség kedvéért maradjunk most annál a változatnál, amikor kiírjuk ezeket. A mérőszám is legyen most publikus adattag.

Az osztály semmi egyebet nem tartalmaz, mint egy mérőszámot. Minden egyéb információ a típusába van kódolva. Az egyparaméteres konstruktor megkapta az explicit jelzőt, hogy a fordító ne használja konverzióra: így nem fog automatikusan semmilyen valós számhoz mértékegységet társítani, hanem nekünk kell minden esetben megadnunk azt.

Az egyszerű használathoz megadhatunk rövid neveket is az egyes típusoknak, pl.:

C++11
using Length = Quantity<1, 0, 0>;         /* hossz, m */
using Area = Quantity<2, 0, 0>;           /* terület, m^2 */
using Mass = Quantity<0, 1, 0>;           /* tömeg, kg */
using Time = Quantity<0, 0, 1>;           /* idő, s */
using Speed = Quantity<1, 0, -1>;         /* sebesség, m/s */
using Acceleration = Quantity<1, 0, -2>;  /* gyorsulás, m/s^2 */
using Force = Quantity<1, 1, -2>;         /* erő, N=m*kg/s^2 */
using Energy = Quantity<2, 1, -2>;        /* energia, J=m^2*kg/s^2 */
using Power = Quantity<2, 1, -3>;         /* teljesítmény, Watt = Joule/s = m^2*kg/s^3 */

Length l1{1.2};     /* 1.2 m */
Time t1{3.7};       /* 3.7 s */

Két mennyiség akkor adható össze, ha megegyezik a mértékegységük. Ez így van bármilyen mértékegységnél. Ezt egy sablon összeadó operátor függvénnyel fejezhetjük ki. Ez azt fejezi ki, hogy két egyforma dimenzió mennyiség (paraméterek) összege egy ugyanolyan dimenziójú mennyiség (visszatérési érték), amely úgy áll elő, hogy a két mérőszámot összeadjuk. A kivonás ugyanígy működik:

template <int M, int KG, int S>
Quantity<M, KG, S> operator+(Quantity<M, KG, S> a, Quantity<M, KG, S> b) {
    return Quantity<M, KG, S>{a.magnitude + b.magnitude};
}

template <int M, int KG, int S>
Quantity<M, KG, S> operator-(Quantity<M, KG, S> a, Quantity<M, KG, S> b) {
    return Quantity<M, KG, S>{a.magnitude - b.magnitude};
}

A szorzás és az osztás trükkösebb. Szorozni és osztani nem csak egyforma dimenziójú mennyiségeket, hanem bármely két mennyiséget lehet, csak az eredmény dimenziója más lesz. A kapott mennyiség dimenzióját azonban a két tényezőből származtatni tudjuk: szorzás esetén a dimenziók is összeszorzódnak, osztás esetén elosztódnak. A mértékegységeket az alapmértékegységek hatványkitevőivel reprezentáljuk, ez a szorzásnál a kitevők összegzéseként fog megjelenni, pl. hossz×hossz=terület, m1×m1=m1+1=m2. A különféle mértékegységek miatt két csoport sablonparaméter kell; az eredmény mértékegysége ezekből levezethető:

template <int M1, int KG1, int S1, int M2, int KG2, int S2>
Quantity<M1+M2, KG1+KG2, S1+S2> operator*(Quantity<M1, KG1, S1> a, Quantity<M2, KG2, S2> b) {
    return Quantity<M1+M2, KG1+KG2, S1+S2>{a.magnitude * b.magnitude};
}

Az osztás ugyanilyen, csak a mérőszámokat osztani, a kitevőket kivonni kell:

template <int M1, int KG1, int S1, int M2, int KG2, int S2>
Quantity<M1-M2, KG1-KG2, S1-S2> operator/(Quantity<M1, KG1, S1> a, Quantity<M2, KG2, S2> b) {
    return Quantity<M1-M2, KG1-KG2, S1-S2>{a.magnitude / b.magnitude};
}

Egyéb operátorok is megadhatók. Minden, aminek van értelme, és amit a programban használni fogunk. Két mennyiség összehasonlítása:

template <int M, int KG, int S>
bool operator<(Quantity<M, KG, S> a, Quantity<M, KG, S> b) {
    return a.magnitude < b.magnitude;
}

Egy mennyiség kiírása általában (valamennyire szépítve):

template <int M, int KG, int S>
std::ostream & operator<<(std::ostream & os, Quantity<M, KG, S> m) {
    os << m.magnitude << ' ';
    bool elso = true;
    if (M != 0) {
        elso = false;
        os << "m^" << M;
    }
    if (KG != 0) {
        if (!elso) os << '*';
        elso = false;
        os << "kg^" << KG;
    }
    if (S != 0) {
        if (!elso) os << '*';
        elso = false;
        os << "s^" << S;
    }
    return os;
}

Egyes mértékegységeknek saját nevük van, ezért a kiírás specializálható. Például az erő (force) mértékegysége a Newton (N):

template <>
std::ostream & operator<<(std::ostream & os, Force f) {
    os << f.magnitude << " N";          /* Newton */
    return os;
}

Ezekkel már megoldhatunk egy feladatot: „Egy 1450 kg-os Ferrari 0-ról 100 km/h-ra 4 s alatt gyorsul. Mekkora ez a sebesség m/s-ban kifejezve, és átlagosan mekkora erő gyorsítja az autót?”

int main() {
    Length l{100 * 1000};   /* 100 km */
    Time hour{3600};        /* óra = 3600 s */
    Time t{4};              /* gyorsulás: 4 s alatt */
    Mass m{1450};           /* az autó tömege: 1450 kg */

    Speed v = l/hour;
    std::cout << "100 km/h = " << v << std::endl;

    Acceleration a = v/t;
    Force f = m*a;          /* Newton törvénye */
    std::cout << "F = " << f << std::endl;
}

Ebben a kis példaprogramban a legfontosabb az, ami nem látszik, amit nem csinál. Az, hogy nem lehet elrontani. Ha bármelyik képletet elrontanánk, pl. F=m*a helyett F=m/a-t írnánk, a program nem lenne lefordítható. Fordítási időben kiderülne a hiba!

Az eddigi program: quantity1.cpp.

Mértékegységek a kódban is

Az eddigiek működnek, típusbiztosak, már csak egy gond van: nem szép a kód. Sőt elég nehéz olvasni; a megszokott l=100 km helyett l{100*1000}-at kell írni. Ha órában adunk meg valamit, 3600-zal kell szorozni, ha lóerőben, akkor 745,7-del. Bár ezeknek az értékeknek adhatunk nevet, a szorzásra nekünk kell emlékezni.

Már a C is támogatott néhány előtagot és utótagot (prefix, suffix) a beépített típusú értékek megadásánál. Az 1.0 érték double típusú, az 1.0f pedig float. A 255 érték tízes számrendszerben adott, a 0377 nyolcasban, a 0xFF pedig tizenhatosban. A C++11 lehetővé teszi azt, hogy saját utótagokat adjunk meg a programba épített literálisokhoz (user-defined literal). Ezeket globális operátor függvényként kell megvalósítani (literal suffix operator).

Ezeket az operátorfüggvényeket az „üres sztring operátorral” kell jelölni: operator "", utána pedig le kell írni az utótagot. Paraméterként a literálist kapják, visszatérési értékük pedig az előállítandó objektum kell legyen. Például ha azt akarjuk, hogy a méter mértékegységet lehessen használni a programban:

C++11
constexpr Length operator "" _m (long double magnitude) {
    return Length(magnitude);
}

Length l = 1.2_m;       /* 1.2 méter */

A Length objektum inicializálásánál a konstruktort kerek zárójellel () hívja a függvény a kapcsos zárójel helyett {}. Ezt azért kell így írni, mert azon a ponton számítási pontosság veszik el (narrowing conversion), a long doubledouble konverzió miatt. A kapcsos zárójeles inicializálás nem engedi ezt meg. A long double-re az utótag operátorokra vonatkozó szabályok miatt van szükség, lásd lentebb.

Ezeknél gyakran előkerül a constexpr kulcsszó is, bár ez nem törvényszerű. A constexpr jelző olyan függvényt vagy változót jelöl meg, amelynek értéke már fordítási időben meghatározható. Így nem futási időben fog megtörténni az átalakítás (hiszen 1.2 méter mindig 1.2 méter lesz, akárhányszor kiértékeljük), hanem már fordításkor. Ehhez a Quantity konstruktora is constexpr kell legyen.

Ha szeretnénk kilométerben megadni egy hosszúságot, esetleg órában az időt:

constexpr Length operator "" _km (long double magnitude) {
    return Length(magnitude * 1000.0);
}

constexpr Time operator "" _h (long double magnitude) {
    return Time(magnitude * 3600);
}

Ezekkel és hasonló függvényekkel együtt a Ferrari gyorsulását kiszámító program így írható:

Speed v = 100.0_km / 1.0_h;       /* 100 km/h */
std::cout << "100 km/h = " << v << std::endl;

Acceleration a = v / 4.0_s;       /* az adott sebességre 4 s alatt */
Force f = 1450.0_kg * a;          /* Newton */
std::cout << "F = " << f << std::endl;

A felhasználó által definiált literálisok

A literális utótag operátorok feladata, hogy valamilyen objektumot hozzanak létre a programkódban megadott literális alapján. Ezeket az operator "" (üres sztring) globális függvénnyel adjuk meg, az alábbi formában:

C++11
OutputType operator "" _suffix(ParamType p);

OutputType x = 123_suffix;

Előtag operátort nem lehet definiálni, kizárólag utótagot. Ezek neve az alulvonás _ (underscore) karakterrel kell kezdődjön; minden más szabványosításra van fenntartva. A C++14-be néhány be is került.

Az operátor a paraméterében kapja meg azt az értéket, ami után írtuk. Számok esetén az értéket két formában kérhetjük, nyersen (raw) és előkészítve (cooked). A nyers forma sztringet, azaz nullával lezárt karaktertömböt jelent: a fenti példában "123" lenne char const *-ként. Az előkészített forma pedig unsigned long long vagy long double típust. Ez mindig a létező legnagyobb ábrázolási tartományú, a 123 így simán 123 lenne unsigned long long-ként.

Sztringek esetén az értéket mindig nullával lezárt karaktertömbként kapjuk, az utótag operátornak azonban ilyenkor nem egy, hanem két paramétere van. Az első egy char const * (vagy más karakter típus, pl. wchar_t), ez tartalmazza a sztringet. A másodikban pedig a karakterek számát kapjuk egy size_t típus értékben – így akár null karakterek is lehetnek a sztringben.

Kódrészlet operator"" _xyz(... milyen paraméterezéssel ...)
123_xyz (unsigned long long) vagy (char const *)
123.0_xyz (long double) vagy (char const *)
"hello"_xyz (char const *, size_t)

Első példa. Az alábbi függvény egy _deg utótagot ad meg, amely egy fokban megadott szöget radiánba vált át. Ez előkészített (cooked) formában veszi át a valós számot, hogy ne kelljen bíbelődni a számjegyek értelmezésével:

C++11
long double operator "" _deg(long double degree) {
    return degree * 3.141592653589793238462643383276/180;   /* pi/180 */
}

std::cout << 90.0_deg << ' ' << sin(180.0_deg);

Második példa. std::string-gé alakító _str utótag; ilyen van C++14-ben operator""s néven:

C++11
std::string operator"" _str(char const *str, size_t len) {
    return std::string(str, len);
}

std::cout << "hello"_str + "vilag"_str;

Úgy tűnhet, hogy ezt a nyelvi elemet kevés helyen lehet használni, esetleg csak tudományos vagy mérnöki feladatokat ellátó programokban. De eredetileg is erre szánták. A C++-nak nagy a részesedése ezekben az alkalmazásokban.

Megjegyzések:

  • Az egész számok előkészített formában mindig előjel nélküliek, mert a literálisok mindig nemnegatívak. A -1 egy kifejezés, az 1 literálissal és az egyoperandusú -, azaz negálás operátorral. Az előkészítésbe a számrendszer megadását is bele kell érteni; 0xFF_x esetén a _x utótag operátor az unsigned long long típusú 255 értéket fogja megkapni.
  • A számok átvételének van egy harmadik formája, amikor egy sablon függvény sablonparamétereiként kapjuk a karaktereket. Erről majd később lesz szó.
  • Ha számot veszünk át raw formában, csak char const * paramétere van a függvénynek. Ha sztringet, akkor char const * és size_t – ez különbözteti meg a kettőt.
  • Ahol lehet, adjunk constexpr jelzőt az utótag operátornak. Amit lehet, fordítási időben kell kiértékelni.
  • Általában pedig: hasonlók a saját literálisok, mint a saját operátorok. Ne erőltessük a használatukat! Csak akkor használjunk ilyet, ha érthetőbb lesz tőle a kód, ne az legyen a cél, hogy tömörebb legyen!

A constexpr jelző

A C és a C++ nyelvben a const jelzővel ellátott értékek nem igazi fordítási idejű konstansok. Ellentmondásnak tűnik, de a konstans változók is csak változók, amelyekre ugyanolyan szabályok vonatkoznak, mint a többi változóra. Például C++-ban nem lehet konstans változóval megadni egy tömb méretét, de nem lehet sablonparaméter sem:

const int i = 12;
int arr[i];         // HIBÁS (kivéve 1-2 kontextust)

const unsigned j = 13;
SomeType<j> x;      // HIBÁS

A const nem azt jelenti, hogy egy érték megváltoztathatatlan, hanem csak egy ígéretet a többi programrész számára: azt az értéket azon a változónéven keresztül nem fogjuk megváltoztatni.

Érezzük azonban, hogy van sok olyan helyzet, amikor valami tényleg konstans, megváltoztathatatlan; sőt akár fordítási időben is meg lehetne határozni az értékét. A fordítási idejű konstansokat a C++11-ben a constexpr kulcsszóval lehet megjelölni. A constexpr azt jelenti a fordító számára: fordítási időben kiértékelendő. Az így megjelölt változók már bárhol használhatók, ahol eddig konstansokból álló kifejezéseket várt a fordító, innen jön a nevük is:

constexpr int i = 12;
int arr[i];         // OK

constexpr unsigned j = 13;
SomeType<j> x;      // OK

constexpr lehet lokális és globális változó is. constexpr lehet osztály statikus változója is, és ez esetben már megengedett, hogy az osztály belsejében adjuk meg annak értékét, nem csak akkor, ha egész típusú:

class X {
    static constexpr double x = 0.19;
};

constexpr lehet még akár függvény is, feltéve hogy a törzse egyetlen return utasításból áll (a C++14-ben lazítottak ezen a szabályon). Ha ciklust szeretnénk, rekurziót is használhatunk:

constexpr unsigned int fact(unsigned int n) {
    return n == 0 ? 1 : n*fact(n-1);
}

A constexpr értékekkel hívott constexpr függvény értéke is constexpr. Így lehetnek az egyes mértékegységekhez tartozó utótag operátor függvények constexpr minősítésűek. És így csökken nullára a futási idejű költség, mert így a mennyiség objektumokat a fordító fordítási időben előállítja, és bit szinten előkészítve kerülnek a lefordított programba.

A program második változata: quantity2.cpp.

Problémák a fizikai mennyiségekkel végzett műveletek során:

  • double ido = 1.2, double hossz = 1.7 => hossz/ido értelmes, és egy sebességet ad, hossz+ido értelmetlen.
  • Eltérő mértékegységek, pl. cm vagy inch.
  • Mértékegység nélüli mennyiségek: pl. fok és radián.

C++ sablon osztály + literális operátorok => minden mennyiség eltérő típus => nem lehet eltérő típusú mértékeket összeadni, szorzások és osztások esetén a fordító kitalálja mértékegységet.

Length s = 56.8_km;
Time t = 1.2_h;
Speed v = s/t;

Futási idejű költsége nulla, a sablonok fordítási időben kiértékelve. A lefordított program ugyanúgy néz ki, mintha nyers double számokkal írtuk volna meg, de az összes átváltás és ellenőrzés automatikus!

5. A névterek és a Koenig Lookup

A most megírt programunk sok nagyon általános nevű típust definiált, pl. Time, Speed, ezek bárhol előfordulhatnak. Az ilyen nevek ütközésének elkerülésére találták ki a névtereket. Érdemes ezért a teljes kódot egy Units nevű névtérbe tenni.

A névterek használatánál a háttérben egy érdekes szabály munkálkodik, amiről kevesen tudnak*, és ez a Koenig Lookup:

Koenig Lookup (Argument dependent lookup, ADL)

Andrew Koenig nagy szerepet vállalt a C++ kidolgozásában, Bjarne Stroustrup mellett. Ő találta ki ezt a szabályt: ha egy függvényhívásban paraméterként egy valamilyen osztálybeli objektumot adunk meg, akkor a fordító a megadott nevű függvényt az objektum osztályát magában foglaló névtérben is keresi.

* Hogy a szabályról kevesen tudnak, az nem sértés akar lenni: egyszerűen úgy van kitalálva, hogy kényelmessé és természetessé teszi a névterek használatát. Ezért legtöbbször fel sem tűnik, hogy egyáltalán létezik.

Kódban ez a következőt jelenti:

namespace NS {
    class A {};
    
    void f(A a) {
    }
}

int main() {
    NS::A x;
    NS::f(x);       /* NS::f-et hívja */
    f(x);           /* using namespace nélkül is NS::f-et hívja!
                     * mert az x típusa NS-beli */
}

Ebben a példában az f(x) függvényhívásnál is rájön a fordító, hogy az NS-beli f() függvényről van szó, anélkül hogy külön kiírtuk volna a hívásnál a névteret, vagy using namespace-t használtunk volna. Mégpedig azért, mert a függvénynek adott objektum az NS névtérből származik, ezért számítani lehet rá, hogy az NS névtérbeli függvénynek szeretnénk paraméterként adni.

A Koenig Lookup akkor fontos igazán, ha olyan függvényhívást szeretnénk csinálni, ahol nem írhatjuk ki a névteret. Ilyenek például az operátorok. Ha van két Math::Matrix típusú változónk, x és y, akkor az x+y kifejezést leírva a fordítónak rá kell jönnie, hogy az operator+() függvényt a Math névtérben is keresse. Ezt nem tudjuk név szerint kiírni (a Math::+ b?! Math::operator+(a, b)...), és ha nem lenne ez a névkeresési szabály, az a használhatóság rovására menne.

A mértékegységes példában így érdemes a teljes kódot a Units névtérbe tenni, a literálisok operátorait pedig még azon belül is a Literals névtérbe:

namespace Units { // Units
    template <int M, int KG, int S>
    class Quantity {
        /* ... */
    };

    template <int M, int KG, int S>
    std::ostream & operator<<(std::ostream & os, Quantity<M, KG, S> m) {
        /* ... */
    }

    namespace Literals { // Units::Literals
        constexpr Length operator "" _m (long double magnitude) {
            return Length(magnitude);
        }
        /* ... */
    }
    
    /* ... */
}

Ez nem rontja a használhatóságot. A főprogram így a következőképp nézhet ki:

int main() {
    using namespace Units::Literals;
    
    std::cout << "Ferrari ===" << std::endl;
    Units::Speed v = 100.0_km / 1.0_h;
    std::cout << "100 km/h = " << v << std::endl;
    
    Units::Acceleration a = v / 4.0_s;
    Units::Force f = 1450.0_kg * a;          /* Newton */
    std::cout << "F = " << f << std::endl;
}

Az egyes mennyiségek típusait minősítjük a névtér nevével, így nincs ütközés a hasonló nevekkel. A kiírásoknál a Units névtérben lévő, mértékegységekhez megadott kiíró operátort a fordító megtalálja a Koenig Lookup miatt, és ugyanez a helyzet a többi operátorral is. A literálisokat kezelő operátort pedig a using namespace Units::Literals-szal láthatóvá tesszük a függvényen belül – de lehetőleg semmiképp sem globálisan.

Az így kialakított változat: quantity3.cpp.

Koenig lookup: ha egy függvényhívásban paraméterként egy valamilyen osztálybeli objektumot adunk meg, akkor a fordító a megadott nevű függvényt az objektum osztályát magában foglaló névtérben is keresi.

namespace NS {
    class A { ... };
    void f(A a) { ... }
}
int main() {
    NS::A x;
    NS::f(x); /* NS::f-et hívja */
    f(x);     /* using namespace nélkül is NS::f-et hívja!
               * mert az x típusa NS-beli */
}