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

1. A megvalósítás

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

2. Példa: egy vektor

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.