Property
Czirkos Zoltán · 2022.06.21.
C# stílusú „property” az objektumokban
C#-ban „property”-nek nevezzük azokat a fajta adattagokat, amelyek írásához és kiolvasásához (get és set) külön műveletet, függvényt társíthatunk. Ezek használata kényelmes és főleg intuitív, mivel úgy viselkednek, mintha publikus, szabadon változtatható adattagok lennének. Tehát míg a szabványos C++-ban egy vektor méretének lekérdezését, és a vektor átméretezését így írjuk:
std::vector<int> v;
v.resize(10); /* setter */
std::cout << v.size(); /* getter */
Ha lenne property nyelvi elem, így írnánk:
myvector v;
v.size = 10; /* setter - átméretezés */
std::cout << v.size; /* getter - méret lekérdezése */
Ilyen nyelvi elem C++-ban nincs. De nem is kell, mert ez felépíthető a meglévőekből! A trükk
csak annyi, hogy a vektorban lévő size
, méret adattag helyett egy olyan objektumot
használjunk, amelynek operátorait alkalmas módon átdefiniálva a tényleges adattagot helyettesítő
objektumként látszódjon.
A fenti példában a size
objektum egy size_t
típusú
adattagot helyettesít (a vektor méretét). Ennek tehát írnunk kell egy .operator=(size_t)
függvényt (mert v.size = 10
jelentése: v.size.operator=(10)
), és egy
operator size_t()
konverziós operátort (cout << (size_t) v.size
).
A megvalósítás lényege a fent említett két operátor megírása. Ezeknek az operátoroknak a feladata, hogy meghívják a tartalmazó objektum megfelelő getter és setter tagfüggvényét, lekérdezés illetve értékadás esetén. Maga a property objektum, amelyik a tényleges adattagot helyettesíti (annak proxy-ja), lényegében nem tartalmaz adatot.
A lenti megvalósításban csak egy referenciát tartalmaz a saját szülő objektumára (a példában ez a vektor). Erre azért van szükség, mert C++-ban egy objektum sosem tudja, hogy egy másik objektumnak részobjektuma-e, és ha igen, nem tudja sehogyan lekérdezni, hogy melyiké. A szülő objektumot viszont ismernie kell, hiszen annak a getter és setter függvényét kell hívni.
A getter és a setter függvényekre a property objektum egy tagfüggvény pointert kap. A tagfüggvény pointerek lehetnek sablonparaméterek is (megengedi a nyelv), és érdemes is így írnunk a kódot, hogy hatékony legyen. (Egy konkrét adattaghoz konkrét getter és setter függvények tartoznak. Ezek azonban lehetnek virtuálisak is.)
Említést érdemel még a „szokványos” értékadó operátor és a másoló konstruktor. Az értékadó operátort a
referencia adattag miatt a fordító automatikusan letiltaná. A vektor használója írhatna olyat, hogy v1.size =
v2.size
(hogy a két vektor legyen egyforma), ezalatt viszont amúgy sem a propertyk értékadását értenénk, hanem a
v2.size
kiolvasását és a v1.size
felülírását. Ezért a property=property alakú értékadó operátort úgy
definiáljuk, hogy tényleg ezt csinálja. A másoló konstruktorban hasonló furcsa helyzet áll elő. Ha nem mondanánk semmit, ez a
referencia másolását jelentené, ami ahhoz vezetne, hogy az újonnan létrehozott objektum property-je a lemásolt objektumra
hivatkozik. Ezért a másoló konstruktort letiltjuk; aki property-t tartalmazó osztályt szeretne másolhatóvá tenni, írjon egy
olyan másoló konstruktort, amelyben az újonnan létrehozott objektum referenciáját adja az új property-nek.
/* property: milyen CLASS-hoz, milyen T típusú adattag,
* melyik SETTERPTR tagfüggvény a setter, melyik GETTERPTR tagfüggvény a getter */
template <typename CLASS, typename T,
void (CLASS::*SETTERPTR)(T const& newvalue),
T (CLASS::*GETTERPTR)() const>
class Property {
CLASS& obj;
/* copy ctor letiltása, hogy muszáj legyen megadni az új szülő
* objektumot (különben másolódna a referencia!) */
Property(Property const&) = delete;
public:
/* a property szülő objektuma. ezt azért kell paraméterként átvenni,
* mert c++ban nem lehet sehogy kitalálni egy objektumról, hogy egy
* másik objektum része-e (subobject) */
Property(CLASS& obj) : obj(obj) {}
/* property beállítása */
Property& operator= (T const& newvalue) {
(obj.*SETTERPTR)(newvalue);
return *this;
}
/* property lekérdezése */
operator T () const {
return (obj.*GETTERPTR)();
}
/* property = property eset kezelése */
Property& operator= (Property const& the_other) {
*this = the_other.operator T ();
return *this;
}
};
Az alábbi vektorban szándékosan nem tárolódik a méret; helyette a lefoglalt memóriaterület elejét és
végét tárolják a pointerek. Ezeket adná a begin()
és az end()
tagfüggvény. Így
látszik, milyen szerepe lehet a getternek – a setteré amúgy triviális.
/* példa */
class Vektor {
private:
/* a memóriaterület eleje és vége. szándékosan két pointer,
* hogy látszódjon, a getter is csinál valamit. */
int* databegin;
int* dataend;
void sizesetter(size_t const& newsize) {
size_t oldsize = dataend-databegin;
int* newdata = new int[newsize];
std::copy(databegin, databegin + std::min(newsize, oldsize), newdata);
delete[] databegin;
databegin = newdata;
dataend = newdata + newsize;
std::cout << "resizing to " << newsize << std::endl;
}
size_t sizegetter() const {
return dataend - databegin;
}
public:
Vektor() : databegin(nullptr), dataend(nullptr), size(*this) {}
int& operator[] (size_t idx) {
if (idx >= sizegetter())
throw "index out of bounds";
return databegin[idx];
}
/* így kell megadni egy propertyt: Vektor osztályhoz,
* size_t fiktív adattag, amelynek a settere és a gettere ez.
* a vektor kliense ezt egy adattagnak látja. */
Property<Vektor, size_t, &Vektor::sizesetter, &Vektor::sizegetter> size;
};
/* példa */
int main() {
Vektor v;
v.size = 5;
v[2] = 1;
for (int i = 0; i < v.size; ++i)
std::cout << v[i] << " ";
std::cout << std::endl;
v.size = 10;
v[7] = 7;
for (int i = 0; i < v.size; ++i)
std::cout << v[i] << " ";
std::cout << std::endl;
}
A teljes kód letölthető innen: property.cpp.