Érték és konstans referencia

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.

1. Melyik?

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ó.

2. Temporális objektumok és konstans referenciák

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.