I/O manipulátorok
Czirkos Zoltán · 2022.06.21.
Az std::setw-hez hasonló manipulátorok írása.
Folytassuk az előadás mértékegységes példáját!
Tekintsünk két egyszerű fizikai problémát: a libikókát és egy doboz vonszolását.
A libikóka akkor van egyensúlyban, ha a két oldalon ható forgatónyomaték (torque) megegyezik. Ezt az erő és az erőkar vektoriális szorzataként tudjuk kiszámolni, ennek mértékegysége az erő×hosszúság miatt Newton×méter, azaz Nm. A doboz vonszolása közben végzett munkát (work), a befektetett energiát az erő és az elmozdulás skaláris szorzásával tudjuk kiszámolni. Itt is egy erő és egy hosszúság szorzódik össze, ennek mértékegyége is Newton·méter, azonban ezt J-vel jelöljük, Joule nevéből.
A két mennyiség (legalábbis az abszolút értékük) mértékegysége tehát megegyezik, ezért a két mennyiséget a C++ programunkban ugyanaz a típus jelképezi. Ez a
számításoknál rendjén is van, azonban ha a kiírást az energia típusra specializáljuk, hogy J
mértékegységben
íródjanak ki az értékek, hirtelen a forgatónyomatékok is elkezdenek J
-ként kiíródni. Amit viszont nem szeretnénk,
mert értelmetlen. Hiába, a fordító nem tudja megkülönböztetni őket, mert a két érték típusa ugyanaz:
using Energy = Quantity<2, 1, -2>; /* energia, J=m^2*kg/s^2 */
using Torque = Quantity<2, 1, -2>; /* forgatónyomaték, Nm=m^2*kg/s^2 */
template <>
std::ostream & operator<< <>(std::ostream & os, Energy m) {
os << m.magnitude << " J";
return os;
}
Torque N = 120.0_N * 1.8_m;
std::cout << N << std::endl; // „216 J” :(
Ezzel a problémával már találkoztunk, és a szabványos könyvtárban néhány esetre meg is van oldva. Például egy számot ki tudunk
írni tízes és tizenhatos számrendszerben is. Pedig a hívás ugyanarra az operátorfüggvényre kell feloldódjon, hiszen mindkét
esetben egész típusú értékkel etetjük az std::cout
-ot:
std::cout << std::hex << 255 << std::endl;
std::cout << std::dec << 255 << std::endl;
Az std::ostream
-ekhez tartozik egy olyan operator<<
overload, amely egy
std::ostream
-et paraméterként váró és visszaadó függvényre mutató pointert tud átvenni. Egy ilyenbe betehetünk egy
olyan programrészt, amely az std::ostream
objektum valamilyen paraméterét átállítja. Az std::hex
is
ehhez hasonlóan működik:
std::ostream & myhex(std::ostream & os) {
os.flags(std::ios::hex);
return os;
}
std::cout << myhex << 15;
Azonban ez most nekünk nem lesz jó, mert nincs olyan tagfüggvénye az std::ostream
-nek, amit meghívhatnánk, és
amivel jelezhetnénk, hogy a saját, Quantity<2, 1, -2>
típusú értékünket hogyan kezelje. Valami más
megoldást kell keresnünk.
Nézzük meg jobban a kifejezést, amit le szeretnénk írni!
Torque N = 120.0_N * 1.8_m;
Energy E = 120.0_N * 1.8_m;
std::cout << newtonmeter << N << std::endl;
std::cout << joule << E << std::endl;
A kétkacsacsőr <<
operátor balról jobbra asszociatív, ezért
a fenti kifejezések lényeges részét így kell értelmezni:
((std::cout << newtonmeter) << N)
Itt az std::cout
típusa std::ostream
, az N
típusa Quantity<2, 1,
-2>
. A newtonmeter
típusát szabadon megválaszthatjuk. Az std::cout << newtonmeter
„kiírás” hatására ugyanis nem kell semmi láthatónak történnie. Sőt nem is szabad, mert a tényleges kiírandó érték az
N
-ben van. A newtonmeter
-nek azért érdemes saját típust adnunk, mert akkor ahhoz egy új
operator<<
függvényt adhatunk meg.
Azonban ez még nem elegendő. Ha a newtonmeter
objektum „kiírása”, azaz a std::cout << newtonmeter
részkifejezés kiértékelésével a függvény a szokásos módon visszatér az
std::cout
-tal, akkor nincs hova feljegyezni azt az információt, hogy a következő Quantity<2, 1,
-2>
-ot newtonméterként vagy joule-ként kell kiírni. Ezért az std::cout << newtonmeter
függvényhívás
visszatérési értékének is egy új, másik típussal kell rendelkeznie.
A működés tehát a következő:
/* 1. */ ((std::cout << newtonmeter) << N) << std::endl
/* 2. */ (TempObj{std::cout, newtonmeter} << N) << std::endl
/* 3. */ (std::cout) << std::endl;
- Először meghívódik az az
operator<<
, amelyik „kiírja” anewtonmeter
-t – legyen annak típusa akármi is. - Aztán ez a függvény visszatér egy olyan objektummal, amelybe bele
van kódolva, hogy melyik
std::ostream
-re, milyen formában kell kiírni a következőQuantity<2, 1, -2>
-t. - A következő
operator<<
azN
-re vonatkozik, de a bal oldalán nem azstd::ostream
van, hanem az ideiglenes objektum. Ez az operátor elvégzi a tényleges kiírást, és visszatér azstd::ostream
-mel. - Az
std::ostream
-mel való visszatérés miatt pedig a kiírás folytatódhat a további<<
operátorok által.
Az első operator<<
függvényt a szokásos módon, globális függvényként kell megvalósítani, mert ennek bal
oldali argumentuma az std::ostream
osztálybeli objektum. A második operátort, amely a tényleges kiírást végzi,
azonban megvalósíthatjuk az ideiglenesen létrehozott objektum tagfüggvényeként is, mivel annak, saját osztályunk lévén, olyan
tagfüggvényt írunk, amilyet csak szeretnénk. A konkrét mértékegységet meghatározó érték lehet akár egy enum
is. A
teljes megvalósítás:
class NewtonMeterPrinter {
public:
enum Unit {
newtonmeter,
joule,
};
NewtonMeterPrinter(std::ostream &os, Unit unit)
: os(os), unit{unit} {
}
/* ez kezeli a második operator<<-t */
std::ostream & operator<< (Quantity<2, 1, -2> q) const {
os << q.magnitude << ' ';
switch (unit) {
case newtonmeter:
os << "Nm";
break;
case joule:
os << "J";
break;
}
return os;
}
private:
std::ostream & os;
Unit const unit;
};
/* ez kezeli az első operator<<-t */
NewtonMeterPrinter operator<< (std::ostream &os, NewtonMeterPrinter::Unit unit) {
return NewtonMeterPrinter{os, unit};
}
Itt könnyű belefutni a C++11 egyik bug-jába, nevezetesen hogy
referencia nem inicializálható {}
szintaxissal. Ez a szabvány szövegezésében
volt egy hiba, amit azóta kijavítottak. Az legújabb fordítókkal ezért működne a fenti
kódban az os{os}
is, ahogy számítunk is rá.
A használat pedig pont úgy néz ki, mint az std::hex
-nél és az
std::dec
-nél:
std::cout << "Energy vs. Torque ===" << std::endl;
Torque N = 120.0_N * 1.8_m;
std::cout << NewtonMeterPrinter::newtonmeter << N << std::endl;
Energy E = 120.0_N * 1.8_m;
std::cout << NewtonMeterPrinter::joule << E << std::endl;
Látszik, hogy az objektumok okosabbak tudnak lenni, mint a függvények. A függvények csak a paramétereiken keresztül látják a külvilágot, az objektumokba azonban bármilyen adatot el tudunk rejteni.
Az így kiegészített változat: quantity4.cpp.
Lóerő vagy Watt?
A teljesítményt (energia/idő) néha wattban (W), néha lóerőben (HP) adjuk meg. 1 lóerő = 745,699872 watt. Specializáld a kiíró operátort, hogy a teljesítményeket általában wattban írja ki! Írj egy I/O manipulátort, amely segítségével lóerőben is kiírható egy teljesítmény! Becsüld meg a programban, hány lóerős lehet a Ferrari (lásd: quantity4.cpp), azaz a 4 másodperces gyorsítás alatt mekkora volt az átlagos teljesítménye!
Bináris számok
Írj bináris szám kiíró I/O manipulátort! A használata legyen az alábbi. Vajon mi lehet itt a bin
típusa?
std::cout << bin << 240 << std::endl; /* 11110000 */
Írj bináris beolvasó I/O manipulátort! Olvassa ez addig a bemenetet, amíg 0
és 1 karaktereket kap. Figyelj arra, hogy a következő karaktert ne egye meg a
beolvasás! Nézz utána az istream::get()
és
istream::putback()
tagfüggvényeknek, hogy lásd, hogyan kell ezt megoldani.
Lehet ezt a programod egyesíteni a bináris kiíró manipulátoros programmal?
unsigned int i;
std::cin >> bin >> i; /* input = 11110000 -> i = 240 */
Számrendszerek
Írj akárhányas számrendszerben kiíró I/O manipulátort!
std::cout << base(2) << 240; /* 11110000 */
std::cout << base(16) << 240; /* F0 */
Melyik az első részkifejezés, amit ki kell értékelnie a fordítónak? Indulj ki ebből!