Érték és konstans referencia
Czirkos Zoltán · 2019.02.27.
C++-ban van két paraméterátadási mód, amik eléggé hasonlítanak egymásra: az érték és a konstans referencia szerinti paraméterátadás. A legfőbb közös tulajdonságuk az, hogy a paraméterként átvett objektumot egyik esetben sem lehet megváltoztatni.
void f(std::string const & s) {
std::cout << s[0]; /* ok */
s[0] = 'A'; /* fordítási hiba */
}
std::string s = "almafa";
f(s);
void f(std::string s) {
std::cout << s[0]; /* ok */
s[0] = 'A'; /* ok, de a másolat változik */
}
std::string s = "almafa";
f(s);
std::cout << s; /* még mindig kis a-val */
A „nem lehet megváltoztatni” azonban láthatóan mást jelent a két esetben. A konstans referencia esetén a változtatás fordítási hibához vezet, hiszen konstans objektumról van szó. Érték szerinti paraméterátadáskor viszont módosulhat az objektum – csak ez az objektum már nem a paramtéterként átvett példány, hanem annak a másolata, tehát az eredeti objektum nem módosul.
Mire jó ez a megkülönböztetés? Gyakran olyan függvényt írunk, amely egy objektumot csak megvizsgál (angolul: observer), annak tartalmára kíváncsi. Például ha megszámlálnánk, hány szóköz van egy sztringben, akkor kíváncsiak vagyunk a sztring tartalmára, de a megszámlálás kedvéért a sztringet fölösleges lenne lemásolni. Ezért konstans referenciát használunk:
int hany_szokoz(std::string const & s) {
int db = 0;
for (size_t i = 0; i < s.length(); ++i)
if (s[i] == ' ')
++db;
return db;
}
Ellenben ha egy olyan függvényre lenne szükségünk, amelyik egy sztringben a szóközöket alulvonásra cserélve állít elő egy új sztringet, akkor ez kevésbé praktikus paraméterátadási mód:
std::string szokozbol_alulvonas(std::string const & s) {
std::string uj = s;
for (size_t i = 0; i < uj.length(); ++i)
if (uj[i] == ' ')
uj[i] = '_';
return uj;
}
A fenti kódot ugyanis valamivel rövidebben is leírhattuk volna. Mégpedig így:
std::string szokozbol_alulvonas(std::string s) {
for (size_t i = 0; i < s.length(); ++i)
if (s[i] == ' ')
s[i] = '_';
return s;
}
A lényeg itt az, hogy az érték szerinti paraméterátadással már eleve létrejön az a munkamásolat, amelyen dolgozhatunk – amely fölött szabadon rendelkezhetünk, mert annak módosítása nem hat majd ki a paraméterként átadott objektumra. Vagyis a függvénybe úgy érkezünk meg, hogy már adott egy sztringünk, amiben csak ki kell cserélnünk szóközre az alulvonásokat, és készen vagyunk.
Ez a megoldás egyébként jobb is, mint az előző. Előfordulhat az, hogy a függvényünknek egy temporális objektumot kell adni:
std::string a = "...", b = "...";
std::string c = szokozbol_alulvonas(a + b);
Ebben az esetben a fordító optimalizálni tud: az összefűzött sztring, azaz a temporális objektum lemásolása helyett magát a temporális objektumot adhatja át paraméterként a függvénynek. Azért teheti ezt meg, mert a temporális objektum nem érhető el más módon: az a + b
kifejezésben jött létre, és csak ott látszik, így aztán szabadon felhasználható.
Fölvet a fenti megkülönböztetés egy érdekes kérdést.
Ha van egy változónk, amelyet átadunk a konstans referencia paraméterű hany_szokoz()
függvénynek, a működés elég magától értetődő. A függvényhívás idejére létrejön egy referencia, amelyen keresztül az eredeti változót, objektumot látjuk.
int hany_szokoz(std::string const & s);
std::string a = "almafa";
std::cout << hany_szokoz(a); /* std::string const & s = a; */
Mi a helyzet akkor, ha ez a függvény egy temporális objektumot kap paraméterként?
std::string a = "...", b = "...";
std::cout << hany_szokoz(a + b);
Azt várjuk, hogy ez működjön: miért ne számolhatnánk meg, a kapott sztringben hány szóköz van összesen? Ennek azonban ellentmondani látszik az, hogy a temporális objektum nem névvel rendelkező változó, márpedig referenciája csak változónak lehet.
Fontos viszont, hogy igazából egyik függvény sem képes megváltoztatni a neki paraméterként átadott objektumot. Sem a konstans referencia, sem az érték paraméterű; az előbbi a konstans, az utóbbi pedig a másolás miatt. Tehát a hívó szempontjából mindegy, hogy konstans referencia vagy érték típusú paramétert vár a függvény.
A paraméterátadás módját a függvény feladata, megvalósítása alapján választottuk ki, azaz hogy szükségünk volt-e munkamásolatra vagy nem. Ez egy implementációs kérdés, ami a függvény megíróját érdekli, a hívóját viszont nem. Ezért úgy döntöttek, hogy ebben a speciális esetben egy referencia lehessen hozzáköthető egy temporális objektumhoz is. De kizárólag abban az esetben, ha konstans referenciáról van szó, hiszen a hívó szempontjából ez fog ugyanúgy viselkedni, mint az értékparaméter. Így a hany_szokoz(a + b)
helyesnek számít.
Vegyük észre, hogy ennek tényleg csak abban az esetben van értelme, ha a referencia konstans. Itt egy olyan függvény, amely a szóban forgó sztringet módosítja (angolul: mutator):
void sztringet_modosit(std::string & s) {
s += '!';
}
Ennek a függvénynek haszontalan lenne egy temporális objektumot adnunk. Mert a temporális objektum úgyis meg fog szűnni – minek akarnánk módosítani, ha a módosítás eredményét úgysem tudjuk már felhasználni sehogyan? Ezért a nem konstans referencia nem képes a temporális objektumhoz kötődni. Az alábbi kódrészlet fordítási hibát vált ki:
std::string a = "hello", b = "vilag";
sztringet_modosit(a + b); /* fordítási hiba */
Sajnos az ilyen kódra a Microsoft C++ fordítója nem jelez hibát, hiába semmi értelme. (Bár ők sosem jeleskedtek a szabványok követésében.) Ez konkrétan az a „misfeature”, ami a legtöbb bosszúságot szokta okozni a házik beadásakor. Vajon miért jelez fordítási hibát a feladatbeadó rendszer, mikor otthon még jó volt? Hát azért, mert nem szabványos a megírt kód, és nem szabványos a fordító sem, amelyik azt elfogadta.