C++ többszörös öröklés és castolás

 ( BaT | 2012. február 22., szerda - 3:46 )

Üdv!

A következő a problémám: adott egy c++ kód, amiben többszörös öröklés is van, természetesen virtuális örökléssel, mivel van közös ős. A kód fordul is szépen csakhogy egyik ponton le kell castolnom az objektumokat a közös ősre, majd később visszacastolni az eredeti típusra. (Ez egy templaten belül van, de ez most lényegtelen.) Lefelé tudok is castolni, viszont felfelé már nem engedi a virtuális öröklés miatt. Csakhogy ha nem virtuálisan öröklök, akkor a többszörös öröklésnél lesz probléma.

Írtam egy programvázat, ami demonstrálja ezt a problémát (kommentekben jeleztem, hogy melyik sorban mi a hiba): http://pastebin.com/FxUq8iRu

A first attempt a virtuális öröklést mutatja, és látszik, hogy a többszörös örökléssel nincs gond, de a felcastolással igen. A second attempt a nem virtuális öröklést mutatja, ahol az látszik, hogy a felcastolással nincs gond, de a többszörös örökléssel igen.

A kérdésem az, hogy fel lehet-e oldani valahogy ezt a problémát, vagy teljesen más megoldás után kell néznem?

Hozzászólás megjelenítési lehetőségek

A választott hozzászólás megjelenítési mód a „Beállítás” gombbal rögzíthető.

Azért "demonstráltad" a dolgot, mert azt hiszed, hogy ennek így kéne mennie, csak a fordító nem jó?
Érted-e, hogy hogyan működik a többszörös öröklés + virtuális öröklés?
Ha igen, érted-e, hogy mi az alapvető probléma azzal, amit szeretnél?

A gond az, hogy amit szeretnel, rossz. Tessek elolvasni a Pragmatic C++-t es utana megkerdezni magadtol, hogy van-e ertelme annak, amit szeretnel.

Addig szabalykent: le meg felcastolgatni altalaban nem szokas egymas utan, mert ez nem villanykapcsolo, hogy akkor most lekapcsolom a fel lakasban a villanyt, majd ujra fel. Meg kell oldanod ugy a tortenetet, hogy ha lecastoltal, akkor az ossel kell dolgoznod tovabb, vagy a kod logikaja rossz.
--
Ki oda vagyik, hol szall a galamb, elszalasztja a kincset itt alant. | Gentoo Portal

"X *ax1=a1;
...
a1=(A1 *)ax1;"
Ez nem jo, mert az X csak az o memoriateruletet latja, az A1-t nem. Meg szep, hogy nem mukodik, hiszen nem tudhatod mi van ott. Ugy kell elkepzelni, hogy van az A1 memoriaterulete. Az X ennek a reszhalmaza. Az A1 latja X-t, az X viszont nem tudja mi van rajta kivul.

"c2->x();"
Melyik x-t akarod hivni? Mert van 2 is.

Rossz a program szerkezete.

Mindhármotoknak: természetesen értem én, hogy melyik megoldás miért nem megy, tehát tisztában vagyok vele, hogy a virtuális öröklésnél miért nem tudok felcastolni, a nem virtuálisnál pedig miért lesz gond a többszörös örökléssel (egyébként is beszédesek a hibaüzenetek).

A probléma ebben a kódban jön elő: http://pastebin.com/A7Ujj2dd
Magyarul ha a konstruktor T*-ot kapna, akkor nem hívhatnám rá x()-et, de szeretném, ha az egyik metódusa T*-ot adna vissza. De még ha az X*-ot is adna vissza, akkor is fel kellene castolnom utána, ami nyilvánvalóan nem fog menni, ha virtuálisan öröklök.

Szerk: aaah oké, ha Z konstruktora T*-ot kap, és csak x() meghívásakor castolom le (vagy akár le se castolom, compiler kitalálja), akkor működik.

Szerk2: eredetileg azért gondoltam, hogy Z a konstruktorában X*-ot kapjon, hogy ezáltal ne is lehessen olyan paramétert átadni, aminek nincs X őse. Egészen addig nem is volt gond, amíg nem alkalmaztam többszörös öröklést. Azonban egy hibajelenség félreértése miatt úgy gondoltam, hogy mindenképpen kell valahol jeleznem, hogy a T az X leszármazottja. Valójában NULL pointerre akartam meghívni egy metódust, ami miatt természetesen elszállt a program.

Köszönöm mindenkinek a segítséget, de azért elméleti szinten érdekelne, hogy a problémát fel lehet-e oldani valahogy, vagy ez a C++ egy tulajdonsága, hogy ezt nem lehet megcsinálni.

A konstruktor vegyen át T*-ot, és ezt T*-ként is tárolja. Az x() tagfüggvényt meg lehet hívni, ha T (vagy valamelyik őse) rendelkezik ilyennel.

Ha konkrétan X felmenőt akarsz megkövetelni (és nemcsak egy x() tagfüggvényt), ráadásul értelmes hibaüzenettel, akkor nézd meg a Derived_from osztályt itt.

template<class D, class B> struct Derived_from
{
  static void constraints(D *d) { B *b = d; }
  Derived_from() { void (*fn)(D *) = constraints; }
};

Használata: a Z(X *) konstruktor törzsébe beleteszel egy ilyet:

Derived_from<T, X>();

Működése:

  1. Létrehoz egy temporális Derived_from objektumot (default konstruktorral), amit majd el is dob.
  2. A Derived_from default konstruktorában egy auto tárolási osztályú függvénypointert hozunk létre, amelynek neve fn. A prototípus, amire mutathat: void visszatérési érték, D* paraméter. A függvénypointert inicializáljuk, mégpedig a Derived_from::constraints() statikus tagfüggvény címével.
  3. Ezt a függvényt in-line definiáljuk. A függvény egy auto tárolási osztályú, b nevű, B-re mutató mutatót hoz létre, amelyet a bejövő (D* típusú) paraméterből inicializál. Ha D a B-nek leszármazottja, akkor ez az inicializáció értelmes, egyébként pedig (ha jól sejtem) constraint violation, ami hibaüzenetet követel a fordítótól.
  4. Ha minden sikeres, akkor (okos fordítónál) jó eséllyel semmi kód sem fog belekerülni az object file-ba a hivatkozási helyen. A vermen létrejön egy objektum, amelynek konstruktora a vermen létrehoz egy pointert, amelybe belerakja egy függvény címét. Aztán az egész megy a kukába a veremről -- nincs látható hatása, nem kell hozzá kódot generálni. (Az elképzelhető, hogy a Derived_from<T, X>::constraints() függvény példányosodik.)
  5. Az fn függvénymutatónak az az értelme, hogy így a d pointert nem kell kézzel megcsinálnunk (és inicializálnunk), pontosabban: nem kell egy D objektumot hamisítanunk a konverzióhoz, hanem tehetünk úgy, mintha kívülről jönne.

Jól illusztrálja szerintem, hogy a C++ gusztustalan.

"Jól illusztrálja szerintem, hogy a C++ gusztustalan."
+10000000000
--
Ki oda vagyik, hol szall a galamb, elszalasztja a kincset itt alant. | Gentoo Portal

"Jól illusztrálja szerintem, hogy a C++ gusztustalan."

-1

Túl hosszú és körmönfont a magyarázatod, felesleges, ennyi is elég lett volna: "pointerek implicit castolásáról van szó."
A fenti osztály ugyanazt csinálja, mint az alábbi ASSERT, csak típus biztosan:

#define ASSERT_BASEOF(TA,TB) {TA* pa = NULL; TB* pb = pa;}

class testa {};
class testb /*: public testa*/ {};

int main()
{
ASSERT_BASEOF( testb, testa );
}

Ettol meg a C++ gusztustalan. Bocs.
--
Ki oda vagyik, hol szall a galamb, elszalasztja a kincset itt alant. | Gentoo Portal

Túl hosszú és körmönfont a magyarázatod, felesleges

Már ne haragudj: csak azt írtam le, amit a Derived_from template class csinál. Hogy miért a Derived_from osztályt ajánlottam, az más kérdés. Egyébként azért, mert Stroustrup weboldaláról származik, ahol azt írja, hogy production code-ban ő ezt használná.

Static assert célra C++-ban makrókat használni egyébként ordenárén nyelvidegen. Arról nem is beszélve, hogy ha bármely makró replacement text-je egy compound statement, akkor ildomos do { ... } while (0)-ba csomagolni, hogy értelmesen lehessen utána a forrásban pontosvesszőt rakni.

"Jól illusztrálja szerintem, hogy a C++ gusztustalan."

Jól illusztrálja, hogy a C++ flexibilis.
Mert ugye vagy ez, vagy egy új nyelvi elem. Ehhez meg az összes hasonlóhoz:
http://en.cppreference.com/w/cpp/types

Egyébként a TR1-től már van is_base_of<A,B> type trait.

"...handing C++ to the average programmer seems roughly comparable to handing a loaded .45 to a chimpanzee." -- Ted Ts'o

+1
A C++ nem csúnya, más nyelvekben erre eleve nincs is lehetőség. A többszörös öröklődés önmagában sok problémát hoz létre, ezeket pedig meg kell oldani valahogy, és minél kevesebb a nyelvi elem, annál jobb. Ha ilyen kódot kell írnod, akkor vagy tervezési hiba, vagy ha nem akkor is dugd el jó mélyre. A kód nagy részének nem kell tudnia a részleteket. A hatékonysághoz néha szükséges egy kis alacsonyszintű hekkelés.
----
Hülye pelikán

"más nyelvekben erre eleve nincs is lehetőség."
Attol fugg, mire gondolsz. A tobbszroros oroklodesre valoban keves pelda van, de az alaposztaly ellenorzesere meg a Javaban is van egyszeru lehetoseg a generikusoknal (? extends BaseClass ha jol emlekszem a szintaxisra, majd jon NagyZ es megmondja).
--
Ki oda vagyik, hol szall a galamb, elszalasztja a kincset itt alant. | Gentoo Portal

A Java generikusokat inkabb ne emlegessuk, az a vilag szegyene. A C# az oke.

----------------------
while (!sleep) sheep++;

Mi a baj veluk?
--
Ki oda vagyik, hol szall a galamb, elszalasztja a kincset itt alant. | Gentoo Portal

Type erasure. Nevetsegesen sok problemat okoz a mindennapokban.

----------------------
while (!sleep) sheep++;

Ez úgy hangzik, mint a C-s void*-os "bohóckodás" (azért idézőjel, mert ott nincs mód másra). De később lett valami normális generikusa is, nem? Vagy rosszul emlékszem?

Ami nagyon jó volt az az ADA generic, az minden tanárnak orgazmust okoz :)
----
Hülye pelikán

static_cast-ot kellene használni, és akkor a fordító szól, ha nem kompatibilisek az osztályok...

Sajnos ez nem jó ötlet.

A példaként megadott kódban eleve static_cast-ot jelent (C++03 5.4 Explicit type conversion (cast notation)), ami jelen esetben nem működik a virtuális ős miatt (C++03 5.2.9 Static cast paragraph 8.)

A dynamic_cast lefordulna, de be kellene bizonyítani (most nem fogom megpróbálni), hogy sosem adna vissza 0-t.

(Szerk: const_cast/static_cast elírás javítva.)

Ha jól értem a példát, szeretnél az ős X -ről felkasztolni. Ugyan nem szép dolog, de néha kellhet, csak kell valami védelem a dologba.

Megoldási javaslatom:
1, kell egy ős osztály, ami nálad az X. Ebbe viszont tárold bele valahogy, milyen osztály is valójában. (nálam erre van egy int benne, annak alsó 8 bitjr egy osztály id, a felső 24 bit, pedig a fontosabb classok (glObject, glLayer, glLayerContainerilyenek), amiknek az öröklését figyelem. Így lecastolva, az int-t megnézve, minden példányról tudom, hogy mi is az. Ezt az új osztályok konstruktorában beállítom. Ezt virtual -isan kell örökölni, ahogy tetted.

2, Kell ebbe az alap osztályba egy virtual függvény: virtual viod dummy(void); Ez majd segíti a fordítót az összekapcsolásban, enélkül nem megy.

3, Felcastoláshoz használd a dynamic_cast -t.

A dynamic_cast hajlamos dobni egy hátast, ha pl nem találja a kapcsolatot egy virtuális függvényen keresztül, vagy nincs is közös ős, ezért vezettem be az int-es azonosítást, így nincs crash, mert csak arra engedem castolni, miről tudom hogy működni fog.

Nem egy szép megoldás, és az én esetemben is meg lehetne oldani másként, de működik.

-az ősről a leszármazottra kasztolást downcast-nak hívják
-a dummy függvény fölösleges, ilyenkor virtuális destruktort szokás létrehozni (ami viszont kvázi kötelező polimorf osztályoknál)
-a dynamic_cast nullpointert ad, ha nincs kapcsolat, illetve ha referenciákat kasztolsz, akkor dobhat kivételt. Nem egy "virtuális függvényen keresztül" keresi a kapcsolatot, hanem az RTTI-t használja, amije akkor lesz egy objektumnak, ha az osztálya polimorffá lett téve.

- van olyan osztály, aminek nincs virtuális destructora, ellenben dummy virtualis függvénye igen.
- ami kvázi kötelező, az nem biztos hogy kell is. Persze lehet vakon követni minden szabályt.
- dynamic_cast nem csak nulllpointert tud visszaadni, hanem crashel. Ugyan mindenhol azt írják hogy nullpointert kapsz, de ez nem így van. (És az nekem lényegtelen hogy miért, de az adott hw-n ahová fordítottam/írtam bizony crashelt, ha olyan castolást akartam, amit nem tudott elvégezni.)
http://stackoverflow.com/questions/278429/what-could-cause-a-dynamic-cast-to-crash
- egy osztály attól lesz polimorf, ha van virtualis függvénye -> ahhoz hogy megtalálja virtuális függvény kell bele. Ok RTTI -n keresztül, amire a trigger mégiscsak az.

Ja crash azért van, mert nincs try catch :)

A linkelt szálban a kommentek között:
"Indeed, 'obj' was freed by another thread and this caused the crash. Thanks for your help ! – Barth Nov 11 '08 at 7:38"

-igen, lehet olyan osztályt írni, aminek nincs virtuális destruktora, de virtuális függvénye igen. ajánlanám bárkinek is az ilyet? nem.
-ez nem vak szabálykövetés. ha tisztában vagyunk vele, hogy mi történhet akkor, ha nem tesszük virtuálissá a destruktort, mégis polimorf osztályként kezeljük, akkor ennek fényében van értelme máshogy csinálni?

Ha már van virtuális függvény akkor van vtábla. Akkor már +1 virtuális függvény nem olyan nagy overhead, és később sok fejfájástól kíméled meg magad, ha mégis kelleni fog az a destruktor. De persze van olyan eset, amikor számít ennyi is, de akkor eleve ne legyenek virtuális függvények.
----
Hülye pelikán

Ha crashel a dyncast, akkor az az implementáció rossz, vagy ahogy lentebb is felfedezték, hibázott a programozó. A dynamic_cast nem crashel, hanem 0-t ad vissza vagy kivételt dob hibás cast esetén.
----
Hülye pelikán

A dynamic_cast implementációja rossz, de ezen nem tudok változtatni. SDK, környezet adott. És crashel, nem 0-t ad vissza.

Tul azon, hogy nem szep megoldas, verzik is. Az Effective C++ -ban pont az ilyen "van egy intem" kezdetu otletekrol mondjak el, hogy miert is nem jo. Tessek beszerezni es elolvasni a konyvet.
--
Ki oda vagyik, hol szall a galamb, elszalasztja a kincset itt alant. | Gentoo Portal

Értem. A könyv azt mondta hogy nem jó. Tudod sok könyv van, és sok mindent mondanak benne. Pl.: most olvastam egy könyvet, ami azt mondta, hogy a gyümölcsfákat metszük meg ősszel. Hát előtte a telken 25 évig a tulajdonos tavasszal metszette a fákat. Sem ő, sem a fák nem olvasták ezt a könyvet.
Ja, gyümölcs nagyon szépen termett rajtuk tavaly is :)

Azért ha egy konkrét fejezetet mondanál, hogy melyikben mondja hogy "van egy intem" az nem jó, azt megköszönném.

Akkor pár hatékonysági mutató, mindegyik feladat 1 milliószor végrehajtva:

1, int osztás, ez 48 msec-ig tart, ez csak úgy viszonyítási alapnak.

2, typeid -vel egy osztály lekérése 240-260msec

3, dynamic_cast -al egy felkasztolás: 1000-1200msec

4, a "valami int" -el osztály ellenőrzése: 136msec

5, new + delete egy olyan osztályra, aminek van constructora és destructora: 56sec

6, new + delete olyan osztályra, aminek nincs deklarálva konstrukora, és nincs virtuális destructora: 54sec /a 2 másodperc konstans megmarad/

7, és ugyanez, ha nincs new és delete, hanem én osztom egy tömbből (van egy construct(), ami beállít egy változót, kvázi konstructor, igazából second phase constructor): 131msec

Hát ezeket nem találtam meg az effective C++ -ban. A "valami int" kétszer hatákonyabb a typeid -nél, és hatszor/hétszer a dynamic_cast -nál.
Mivel 3 objektum típus a program élete során eléggé fluktuál, ezért az elején foglalok nekik egy-egy tömböt, és onnan én osztom. Kegyetlen gyors. Nincs memória töredezés. Nincs elefelejtett felszabadítás (végén a 3 tömbbre nekem kell ügyelni, de ez belefér). Ezért nincs még virtuális destructorom sem, mert nem használok new -t és delete-t erre a három classra (szépen crashel is delete -re :) ).
56sec vs 131msec.. akkor ez nem effective? Mert az effective C++ -ban nincs leírva?

Jó text book, szép szabályos C++ kódot lehet írni belőle. De azért néha van az adott feladatra jobb, hatékonyabb (itt sebesség szempontjából) megoldás. Mégha nem is annyira szép.

Nagyon keves olyan feladat van, ahol az objektumok letrehozasanak sebessege ennyire kritikus lenne. Ellenben nagyon sok olyan van, ahol szamit a kesobbi karbantartas koltsege. Ezek szerint belefutottal egy olyanba, ahol ez tenyleg ennyire kritikus, es nem a szep, hanem a gyors megoldast kellett valasztanod. Elofordul ilyen, es - ha tenyleg ilyen volt - jo, hogy felismerted, de altalanossagban ez az int-es trukkozes kevesbe ajanlott.

--
R2D2 a filmtörténet legmocskosabb szájú karaktere.
Minden szavát kisípolták.

az eddigi infók alapján el tudom képzelni, hogy miért erőszakolod meg a nyelvet, de azért akkor is.. a kód mennyire publikus?

Az alapvető félreértés ott van, hogy az Effective C++ címe nem a sebességre hanem a fejlesztési-karbantartási időkre vonatkozik.

A következő probléma, hogy olyanokat hasonlítasz össze mint a saját int, a typeid, meg a dynamic_cast, de a gond az, hogy az esetek 99%-ban ezekre nincs szükség, öröklődéssel és virtuális fv-ekkel ugyanaz megoldható sokkal szebben és kényelmesebben. Sebesség szempontból az a plusz egy indirekció elhanyagolható, egyedül az inlineosítás hiánya miatt lesz mérhető különbség.

Egyébként az Effectív C++ 39.-es tanácsa tárgyalja a kérdést.

A memory pool és placement new áldásos hatása különösen kis objektumokra annyira ismert és általános, hogy Stroustrup C++ "bibliája" is tárgyalja. De alapvetően semmi köze a dynamic_cast kérdéskörhöz.

Az meg hogy a dynamic_cast bugos egy 2000 utáni széles körben használt fordítóban, az olyan nonszensz feltételezés, hogy nehéz rá bármit mondani. Talán annyit, hogy ne használd inicializálatlan vagy felülírt memóriaterületre...

"...handing C++ to the average programmer seems roughly comparable to handing a loaded .45 to a chimpanzee." -- Ted Ts'o

Nem lehetne kikerülni inkább a többszörös öröklődést, és compositionnel megoldani a dolgot?
_Biztos_, hogy ahol neked kell, ott mindkét felmenő ágra igaz az -is-a- reláció?

Mint már fent írtam, végül sikerült megoldani a problémát (legalábbis ezt, volt más is).

Az eredeti kódban A és B inkább interface volt csak, mint osztály, persze c++-ban nincs igazán megkülönböztetve a kettő. Tehát teljesült az is_a kapcsolat.

A gond ott jött elő, hogy ha egy osztály A-t és B-t is örökölte, majd az objektumra mutató pointer le lett tárolva egyszer A*-ként, egyszer B*-ként, akkor a felszabadítással problémák adódtak. Ezért írtam egy saját auto_ptr megoldást (abba nem mennék bele, hogy miért kellett sajátot írni), abból is olyat, ami megköveteli, hogy az auto_ptr-ben használt osztály leszármazzon egy bizonyos osztályból (ez az X). A felkasztolás az auto_ptr-ben lett volna, aminek a konstruktorában megfelelő típusú pointer átadása lett volna kikényszerítve (a megoldás végül az volt, hogy erről lemondtam).
További probléma magával a felszabadítással volt, egyszerűen amikor meghívtam a virtuálisan öröklő objektumra a delete-et, elcrashelt a program. Hogy egész őszinte legyek, nem merem azt mondani, hogy nem én szúrtam el, de mivel határidős feladat volt, végül egyszerűbbnek láttam teljesen máshogy megoldani a problémát.

Szerk: ha gondoljátok, írok példakódot (bonyolult lenne megfogalmazni), mert azért érdekel, hogy tényleg én rontottam-e el, vagy sem. De arra most nincs időm.

Szerk2: megvan mi volt elrontva, lemaradt egy virtuális destruktor...