Omistamine, kopeerimine ja polümorfism
Kui klass on lihtne, ei pärine ühestki teisest klassist, või kui pärineb ja sinna hulka ei ole segatud polümorfismi, on lihtne otsustada, kas omistamis-operaator ja kopeerimiskonstruktor lisada klassile või mitte. Kui aga polümorfism mängu tuleb, ei ole enam asi nii selge (vähemalt tundub nähtud koodi põhjal nii).
Lihtne reegel oleks see, et keela kopeerimine ja omistamine – deklareeri privaatsena, aga jäta defineerimata omistamis-operaator ja kopeerimis-konstruktor.
C++’is on polümorfisme erinevaid. Järgnevalt tuleb juttu dünaamilisest polümorfismist ehk klassidest, mis sisaldavad virtuaalseid või abstraktseid funktsioone ning tuletatud klassides kirjutatakse need üle ja objekte kasutatakse baasklassi viida või viite kaudu.
Kopeerimine
Miks keelata polümorfsete klasside kopeerimine? Et sellele küsimusele vastata, tuleks vaadata, mida teeb kopeerimis-konstruktor:
struct C : Base { C(C const& other) : Base(other) , member1(other.member1) , ... { } }; struct D : C { ... };
Kopeerimis-konstruktor konstrueerib baasklassi objekti ja kõik klassi liikmed nende kopeerimis-konstruktorite abil. Kui C kopeerimis-konstruktorile anda viide D objektile, siis on probleem, sest sel juhul ei konstrueerita D objekti koopiat vaid selle D objekti C alamobjekti koopia. Kui polümorfismi kasutatakse, siis üldjuhul ei kasutata konkreetseid tüüpe ja probleemid on lihtsad tulema. Aga ilma kopeerimiseta ka päris ei saa ja selleks puhuks on õnneks lahendus olemas – nimelt kloonimine.
Kloonimine
Kloonimine on sisuliselt kopeerimine, ainult et kopeerimis-konstruktori otse kasutamise asemel on vahel virtuaalse funktsiooni välja kutsumine. See virtuaalne funktsiooni väljakutse ongi see, mis võimaldab meil üles leida õige kopeerimis-konstruktori ja objektist õiget tüüpi koopia teha. Tavaliselt kasutatakse sellise funktsiooni nimeks clone(), aga see ei pea nii olema. Selleks, et clone() oma tööd teha saaks, on vaja kopeerimis-konstruktorit, aga see tuleks deklareerida kui protected – privaatseks ei saa teda teha, sest siis ei ole võimalik kopeerida sellest klassist tuletatud klassi objekte ja avalikuks ei ole ka hea mõte teha eelmises peatükis kirjeldatud põhjustel. clone() implementeerimisel tuleks kindlasti jälgida seda, et this viitaks ikka täpselt seda tüüpi objektile, mille klooni parajasti tehakse. Kui see nii ei ole, siis on juhtunud nii, et tuletatud klassi autor on unustanud clone() üle kirjutada. clone() üle kirjutamata jätmine on probleem, sest nagu otse kopeerimis-konstruktori kasutamise puhulgi, võib juhtuda, et kloonitakse ainult baasklassi alam-objekt. Järgnev koodijupp demonstreeribki kõike eelnevalt kirjutatut.
class Base { protected: Base(Base& other) { ... } public: virtual Base* clone() const { // derived classes must override clone! assert(typeid(*this) == typeid(Base)); return new Base(*this); } }; class D1 : public Base { protected: D1(D1 const& other) : Base(other) { } public: virtual D1* clone() const override { // derived classes must override clone! assert(typeid(*this) == typeid(D1)); return new D1(*this); } }; class D2 : public D1 { // viga - clone on üle kirjutamata. Kui seda D2 objekti kloonida, // kutsutakse välja D1::clone ja assert ütleb, et probleem on. }; void f(Base& b) { Base* x = b.clone(); delete x; } void g() { D1 d1; D2 d2; f(d1); // 1 f(d2); // 2 }
Omistamine
Miks omistamis-operaatorit ei saa kasutada?
Põhjus on lihtne – sa ei tea, mis tüüpi objekt on vasakul pool omistamis-operaatorit. Selle peale võib tekkida mõte, et võiks virtuaalse omistamis-operaatori teha, sest siis ju kutsutakse õige omistamis-operaator välja nagu virtuaalsete funktsioonide puhul ikka. Jah, seda küll, aga sa ju ei pruugi teada ka seda, mis tüüpi objekt on paremal pool omistamis-operaatorit. Kui omistamis-operaatoris hakata kontrollima, mis tüüpi objekt paremal pool võrdusmärki on, peaks see operaator kuidagi hakkama saama ka olukorraga, kui see parempoolne objekt ei ole sobivat tüüpi. Kuna omistamis-operaatorist ei ole võimalik ebaõnnestumisest märku anda muul moel kui erindit (i.k. exception) visates, on tulemuseks kood, kus erindid lendavad väga ootamatutel hetkedel. Parem juba eos vältida selliseid asju.
Kui omistamis-operaatorit ikka väga vaja on, siis tuleks vaadata üle oma disain ja leida mõni parem lahendus.