A szorzást és az indirekciót is a * operátor jelöli. Vajon mit jelent az alábbi kódrészlet?
A * B;
A válasz: mindkettő lehet. Ha A egy típus, akkor B egy ilyenre mutató pointer lesz:
int * p, tehát a sor egy objektumdefiníció. Ha A egy változó, akkor B-nek
már létező változónak kell lennie, és a kód egy szorzást jelent.
Vegyük észre, mekkora különbség van a kettő között! Az első esetben ez a sor hozza létre a B
változót. A második esetben már léteznie kell. Sőt ez a sor akár ugyanabban a függvényben kétszer is
szerepelhet, két különböző értelemben:
#include <iostream>
class X {};
void operator*(X a, X b) {
std::cout << "szorzas\n";
}
X A, B;
int main() {
A * B;
using A = int;
A * B;
B = new int;
}
Ez azért nehezíti meg a fordítók (íróinak) életét, mert a sorról szintaktikai szabályok alapján nem dönthető el, hogy mit jelent, csak a szimbólumtábla (változónevek, típusnevek stb.) vizsgálatával.
Adott az alábbi kódrészlet:
template <typename T>
class A {
/* ... */
};
template <typename T>
void fv() {
A<T>::B * C;
}
Tehát adott egy sablon osztály, és egy sablon függvény. A függvény példányosítja a sablon osztályt
valamilyen típussal, aztán ::B és * C. Az eddigiekből tudhatjuk, hogy ez gyanús:
valami * valami lehet szorzás és pointer is. Vajon melyik? Azt is mondtuk, hogy ez attól függ,
mi van a * előtt, típus vagy változó. De vajon melyik? Ez meg attól függ, hogy mi van az A
sablon osztályban, statikus változó (mert azt is a ::B szintaxissal érjük el), vagy belső típus
(azt is ::B szintaxissal). Ha változó, akkor ez egy szorzás, ha típus, akkor C egy
pointer lesz. Tehát például az alábbi kódrészletben pointer lesz, gondolhatnánk:
template <typename T>
class A {
public:
using B = char;
};
template <typename T>
void fv() {
A<T>::B * C; // = char * C?
}
A helyzet az, hogy nem. Ebből a kódrészletből nem látszik egyértelműen, hogy C pointer lesz.
Ugyanis attól még, hogy az alap sablon osztálynak (base template) egy B nevű belső típusa van,
nem biztos, hogy nem lesz olyan specializációja, amin belül B nem egy statikus változó:
template <>
class A<int> {
public:
static int const B = 3;
};
Ebben az esetben A<double>::B a char típus szinonimája, A<int>::B
pedig egy konstans, aminek az értéke három. Ugyanez a helyzet az fv() sablonnal is: double és int
példányosítás esetén látszólag mást jelent a kód. Az A<T>::B ezért egy ún. függő név (dependent name).
A félreérthetőséget úgy oldották fel, hogy az ilyen függő neveket alapesetben statikus tagváltozónak tekintik. A fordítónak
külön jelezni kell, ha ilyenkor típusról beszélünk, a typename
kulcsszóval:
template <typename T>
void fv1() {
A<T>::B * C; // szorzás
}
template <typename T>
void fv() {
typename A<T>::B * C; // pointer
}
Erre sablon kódban mindig figyelnünk kell.
Nézzük meg az A<B>::C alakú kódrészletet egy kicsit jobban. Vajon mit jelent az alábbi kód?
A<B>::C
Ez egy A nevű sablonosztály lenne példányosítva B típussal, amin belül C egy statikus
változó? Vagy inkább az A < B kifejezés értékét vizsgáljuk, hogy nagyobb-e, mint a globális ::C
változó? Ez megint attól függ, hogy az A típus (sablon osztály) vagy változó neve.
C-ben a (), [], {} zárójeleket használtuk különféle célokra. C++-ban
a <> karakterpárok is zárójelekként funkcionálnak sablon kódrészletekben. A gond a szokásos:
ezeknek a karaktereknek más jelentése is van, operátorok nevei.
Tegyük fel, hogy X egy sablon osztály,
egy darab, int típusú sablonparaméterrel. Van két fordítási idejű egész konstansunk, A és B.
Az X osztályt ezek közül a nagyobbiknak az értékével szeretnénk példányosítani:
template <int N>
class X {};
enum { A = 5, B = 7 };
int main() {
X< A>B ? A : B > x; // syntax error
}
Ez nem fog menni. Az első > karaktert a fordító a sablon bezáró zárójelének hiszi. Így,
látszólag fölöslegesen, zárójeleznünk kell:
X< (A>B ? A : B) > x; // ok
A > karakter nem csak a sablon kód bezáró zárójele és összehasonlító operátor. Két
egymás utáni kacsacsőr >> egy másik operátort, a bitenkénti léptetés operátort
reprezentáló token. Vagy mégsem.
Az alábbi kódrészlet C++98-ban helytelennek számított, mert a fordító a két bezáró kacsacsőrt léptető operátornak hitte. Csak úgy lehetett lefordítani, ha szóközt tettünk közéjük.
std::vector<std::vector<int>> v;
C++11-ben már le szabad írni ezt szóköz nélkül. A szintaktikai elemzés után dől csak el,
hogy a >> karaktersorozatot hogyan értelmezi a fordító, egy operátorként vagy két zárójelként.
Adott az alábbi kódsor. Ez ránézésre egy objektumdefiníció: az std névtérben megtalálható function
osztálysablont példányosítja int() függvénytípussal, és az így megadott osztályból létrehoz egy objektumot f
néven:
std::function<int()> f;
No igen, de mindez csak akkor igaz, ha tényleg ilyen típusokról van szó. Mi a helyzet akkor, ha a function és
az f nevek definíciója az alábbi? Ebben az esetben a kódsor egy kifejezés! std::function, a globális változó, kisebb-e
int() nullánál, és az így kapott érték nagyobb-e f-nél.
namespace std {
int function = 0;
};
int main() {
int f = 0;
std::function< int() > f;
}
Ez ugyanaz a probléma, mint a typename kulcsszó esetében: ha nem tudjuk, mi az az std, akkor azt
sem tudhatjuk, hogy az abban lévő function egy sablon, amit a kacsacsőrök példányosítanak. Lehetne változó is, és
akkor a kacsacsőrök összehasonlítást jelentenének. Márpedig sablon kódnál nem tudhatjuk! Ezért ha egy sablonosztály statikus
függvényét, vagy akár egy sablon típus sablon tagfüggvényét szeretnénk meghívni, akkor a template kulcsszóval
jelezni kell, hogy egy sablonról van szó (innen fogja tudni a fordító, mit jelentenek a kacsacsőrök):
template <typename T>
void func(T obj, T* pobj) {
T::template f<int>(); // statikus fv
obj.template g<int>(); // tagfv
pobj->template g<int>();
}
C++98-ban az osztályok nevei függvényként is használhatóak. Az OsztályNeve(kifejezések...) alakú
kifejezés konstruktorhívást jelent. Ugyanígy kerek zárójellel kell megadni a névvel is rendelkező objektumok definíciója esetén
a konstruktorparamétereket. Csakhogy...
class Complex {
Complex(double re = 0, double im = 0);
};
Complex c1(2, 3); // objektumdefiníciók
Complex c2(2);
Complex c3(); // függvénydeklaráció
Figyeljük meg az utolsó sort! Ez típusnév név(); alakú. int fv(); – ismerős? Akár egy
függvénydeklaráció is lehetne. Sőt tényleg az, mert a szintaktikai kétértelműséget úgy kerülték meg, hogy azt mondták, ami
függvénydeklaráció és objektumdefiníció is lehetne (tehát amire ráillik mindkét nyelvtani szabály), az függvénydeklaráció. A
paraméter nélküli esetben, ha objektumot szeretnénk, el kell hagynunk az üres zárójelpárt (amit amúgy függvénynél nem tehetnénk
meg). Ez csak azokon a helyeken van így, ahol nem biztos, hogy konstruktorhívásról van szó:
Complex c3; // objektumdefiníció, tilos ()
cout << Complex(); // temportális objektum, kötelező ()
Complex* p1 = new Complex; // itt mindkettő jó
Complex* p2 = new Complex();
Ez a hírhedt „most vexing parse” probléma, a C++ egyik legbosszantóbb szintaktikai kétértelműsége. Sok esetben lefordítható kódhoz juthatunk mindkét változattal:
std::string s1;
std::cout << s1; // sztring kiírása
std::string s2();
std::cout << s2; // bool(true) kiírása, mert függvénypointer->bool cast
Egy összetettebb példa, ami szintén függvény lesz, lent látható. A felső sor is lefordul (hiába nincs két komplex számot
átvevő konstruktor!), mert érthető függvénydeklarációként. Az így írt függvénydeklaráció igazi jelentését az alsó sor mutatja:
egy komplex számot, és egy függvénypointert átvevő függvény deklarációja. Az első azért lehetséges, mert ebben a kontextusban
Complex(c1) és Complex c1 ugyanazt jelenti; a c1 nevű paraméter típusa komplex szám.
(A zárójelnek itt precedenciamódosító hatása lenne, de épp nem csinál semmit. Kifejezésekben bárhova írhatunk zárójelet!
A main() függvény fejlécét is írhatjuk int (main)(int (argc), char** (argv)) alakban.) A második
rész, a Complex() egy függvény típust ad meg (Complex visszatérési értékű, paraméter nélküli függvény),
de paraméterátadáskor a függvény helyett függvényre mutató pointert értünk. Így végülis az egész érthető függvénydeklarációként.
Complex c4(Complex(c1), Complex()); // a kód
Complex c4(Complex c1, Complex(*)()); // a jelentése
Complex c4((Complex(c1)), Complex()); // javítva: c4 objektum definíciója
Javítani a szokásos módon, zárójelezéssel lehet. (Complex(c1)), ezt nem lehet egy függvény paramétereként érteni,
mert a formális paraméter megadása egy deklaráció típusnévvel kell kezdődjön, nem zárójellel. A fenti kódrészlet erőltetettnek tűnik, de ugyanilyen probléma
bármikor könnyen előjöhet. A következő kódrészletben egy fájlból (itt: std::cin) olvasnánk be egész számokat
egy vektorba, istream_iterator-ok segítségével. De nem megy, mert a jelzett sor szintaktikailag ugyanaz, mint
a fenti: egy függvénydeklaráció.
using namespace std;
// ez hibás...
vector<int> numbers(istream_iterator<int>(cin), istream_iterator<int>());
copy(numbers.begin(), numbers.end(), ostream_iterator<int>(cout, "\n"));
Javítani zárójelezéssel, vagy C++11-ben {} szintaxissal lehet:
// így már jó
vector<int> numbers((istream_iterator<int>(cin)), istream_iterator<int>());
// meg így is
vector<int> numbers{istream_iterator<int>(cin), istream_iterator<int>()};
Van egy sablon osztályunk, amelynek két int paramétere van. Mint pl. az alábbi Max osztály: ez
statikus adattagként tartalmazza a két szám közül a nagyobbikat. Tesztelésképp kiírjuk az összehasonlítás eredményét
(print()). Aztán szeretnénk automatizálni is a tesztet: az assert makró megszakítja a programot, ha
hamis kifejezést kap.
#include <cassert>
#include <iostream>
template <int A, int B>
struct Max {
static const int value = A>B ? A:B;
};
void print(bool b) {
std::cout << (b ? "igaz" : "hamis");
}
int main() {
print(Max<5, 7>::value == 7);
assert(Max<5, 7>::value == 7);
}
A print()-es sorral semmi baja a fordítónak, azonban az assert()-es sor szintaktikai hibás.
Miért? Pedig az assert() makrónak egy paramétere van!
Hát igen, egy paramétere, de az előfeldolgozó nem ismeri a sablonokat, és az azok által használt <>
zárójelezést. Ezért az azt hiszi, hogy az assert() makrót két paraméterrel próbáljuk hívni, az
első Max<5, a második pedig 7>::value == 7. Innen jön a hibaüzenet. A javításhoz
be kell zárójeleznünk a kifejezést:
assert((Max<5, 7>::value == 7));