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 int
→char
és a double
→int
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!
„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:
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:
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
:
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:
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}; */
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:
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:
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;
}
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.
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.:
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:
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 double
→double
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:
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:
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:
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, az1
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 azunsigned 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, akkorchar const *
éssize_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!
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 */
}
- Arne Mertz: Use Stronger Types!
- Strong types for strong interfaces
- Vincent Zalzal: Getting the Benefits of Strong Typing in C++ at a Fraction of the Cost
- Jonathan Boccara: Implementation of strong types in C++
- Bjarne Stroustrup: C++11 Style – A Touch of Class, Going Native 2012 Keynote Speech (Youtube).
- Herb Sutter: GotW #30: Name Lookup.
- Gimli Glider (Wikipedia).
- Mars Climate Orbiter (Wikipedia).