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));