Fórum Root.cz
Hlavní témata => Vývoj => Téma založeno: bjarne 03. 12. 2015, 14:15:35
-
Ahoj,
budu mit třídu exportovanou dynamickou knihovnou, např. class TridaZKnihovny.
Pokud v programu který tuto knihovnu používá vytvořím proměnnou
TridaZKnihovny* instance = new TridaZKnihovny();
tak mi kompilátor vygeneruje kód závislý na velikosti, kterou odvodí třeba z hlavičkového souboru.
Takže když bych updatoval tu dyn. knihovnu, tak mi to může rozbít ten program, protože se velikost toho
typu může změnit.
Teď ta otázka: Potřeboval bych teda zaručit, že new alokouje správnou velikost. V C++ můžete přetížit operátor new.
Nějak takhle:
class TridaZKnihovny {
void* opeator new(size_t size) { return malloc(sizeof(TridaZKnihovny)); } // ignoruju size
}
Všude jsem našel různý příklady jak někdo přetěžuje ten operátor aby pak alokoval paměť z nějaké předalkovoané oblasti, takže rychlejší alokace, ale nikde jsme neviděl, že by někdo ignoroval ten parametr size a sám si tu velikost určil.
Nemůžu nikde najít, jestli je zaručené, že ten argument size není nějak rozšířen o místo, co potřebuje třeba standardní knihovna pro správu paměti nebo tak něco. Jako kdyby to bylo tak, že size = sizeof(TridaZKnihovny) + dodatecna_pamet, tak to nebude fungovat, protože já pak alokouju jen sizeof(TridaZKnihovny).
Snad mi rozumíte, o co mi jde.
-
Jsem to udělal blbě ta metoda nesmí být inline, jinak by se nemusela vůbec volat a překladač by to nahradil přímo tím kódem. Ale to je jedno.
-
A k cemu to ma byt dobre ? Resp. co vsechno ma byt soucasti ABI ?
Osobne bych pridal static metodu, napr.:
class TridaZKnihovny
{
static TridaZKnihovny* create()
{
return new TridaZKnihovny();
}
}
Ale obecne to na zmeny ABI nestaci.
-
Kazdopadne, "new" nema v modernim C++ co pohledavat. Misto toho se pouzivaji smart pointery, ktere pocitaji reference a zaridi dealokaci.
-
Kazdopadne, "new" nema v modernim C++ co pohledavat. Misto toho se pouzivaji smart pointery, ktere pocitaji reference a zaridi dealokaci.
To bych si nebyl tak jistej, protože smart pointery jsou interně implementované pomocí CAS instrukce na dané platformě a to je prostě z principu pomalejší. Rozhodně bych nechtěl mít celý program protkaný CAS interukcema. Bych řekl, že ta CAS operace může být třeba 100x pomalejší než normální kopírování.
Použít, kde je to třeba, ale nepřehánět to.
-
Děláš to špatně, dynamická knihovna by měla mít čistě abstraktní interface, např:
class TridaZKnihovny
{
virtual void metoda1() = 0;
virtual void metoda2() = 0;
};
a taky by měla mít ta dynamická třída metodu, kterou vytvořiš instanci, něco jako:
std::shared_ptr<TridaZKnihovny> vytvor_tridu_z_knihovny();
Tím zaručíš, že změnou ve zděděné třídě, která dělá implementaci, nerozbiješ ABI. Ale i tak je k ABI potřeba přistupovat opatrně, např. nemůžeš měnit pořadí virtuálních metod, protože to rozbije vtable. Více viz https://techbase.kde.org/Policies/Binary_Compatibility_Issues_With_C++
-
A k cemu to ma byt dobre ? Resp. co vsechno ma byt soucasti ABI ?
Osobne bych pridal static metodu, napr.:
class TridaZKnihovny
{
static TridaZKnihovny* create()
{
return new TridaZKnihovny();
}
}
Ale obecne to na zmeny ABI nestaci.
Potřeboval bych vědět, že můžu tímhle upůsobem instancovat třídu z knihovny i když se třída v dalších verzích knihovny může změnit svoji velikost.
Tu statickou metodu create bych právě diky tomuhle neměl potřebovat.
-
Děláš to špatně, dynamická knihovna by měla mít čistě abstraktní interface, např:
class TridaZKnihovny
{
virtual void metoda1() = 0;
virtual void metoda2() = 0;
};
a taky by měla mít ta dynamická třída metodu, kterou vytvořiš instanci, něco jako:
std::shared_ptr<TridaZKnihovny> vytvor_tridu_z_knihovny();
Tím zaručíš, že změnou ve zděděné třídě, která dělá implementaci, nerozbiješ ABI. Ale i tak je k ABI potřeba přistupovat opatrně, např. nemůžeš měnit pořadí virtuálních metod, protože to rozbije vtable. Více viz https://techbase.kde.org/Policies/Binary_Compatibility_Issues_With_C++
Já nevím, proč bych měl mít tu factory metodu. Když přetížím operátor new, tak by to mělo stačit? Berme to teď čistě z hlediska alokace potřebné paměti.
S datovými členy se bude z vnějšku té exportované třídy pracovat jen pomocí getteru/setteru a žádná veřejná metoda nebude nikdy modifikována.
Podle mě takhle je problém čístě v té velikosti při provádění alokace?
Každopádně si pročtu ten KDE dokument.
-
Já nevím, proč bych měl mít tu factory metodu. Když přetížím operátor new, tak by to mělo stačit?
Protože to co chceš udělat, je prasárna. Jak víš, že dynamická knihovna používá stejný alokátor, jako aplikace? Jak zaručíš, že se ta tebou vytvořená třída správně inicializuje? Co když bude konstruktor inlinovaný?
-
Pozrel by som sa ako na to ide Qt ktora ma velmi dobru ABI kompatibilitu. Tam ma kazda trieda len jednu privatnu member premennu a to je pointer na instanciu privatnej triedy ktora drzi vsetko potrebne member premenne. Tym padom je potom velkost public triedy velmi stabilna napriec verziami kniznice. A ziadnu factory metodu nepotrebujete.
-
Já nevím, proč bych měl mít tu factory metodu. Když přetížím operátor new, tak by to mělo stačit?
Protože to co chceš udělat, je prasárna. Jak víš, že dynamická knihovna používá stejný alokátor, jako aplikace? Jak zaručíš, že se ta tebou vytvořená třída správně inicializuje? Co když bude konstruktor inlinovaný?
Ten přetížený new operátor bude implementovat ta třída z té knihovny. Tu architekturu jak se ty knihovny budou vytvářet tak aby co nejlépe fungovaly s aplikací vymýšlím já, tak bych právě potřeboval zajistit to, aby exportované třídy z knihovny mohli měnit svoji velikost a taky strom dědění.
Třeba se do exportované třídy A přidá další datový člen a další bázová třída a mělo by to fungovat bez nutnosti rekompilace kódu na té knihovně závislé.
-
Qt to ma zhruba takto
// A_p.h
class A_p
{
int a,b;
void metoda1();
void metoda2();
}
// A.h
class A_p;
class A
{
A_p *d;
public:
A();
void metoda1();
void metoda2();
}
// A.cpp
#include "A.h"
#include "A_p.h"
void A_p::metoda1()
{
}
void A_p::metoda2()
{
}
A::A() : d(new A_p())
{
}
A::~A()
{
delete d;
}
void A::metoda1()
{
d->metoda1();
}
void A::metoda2()
{
d->metoda2();
}
-
Pozrel by som sa ako na to ide Qt ktora ma velmi dobru ABI kompatibilitu. Tam ma kazda trieda len jednu privatnu member premennu a to je pointer na instanciu privatnej triedy ktora drzi vsetko potrebne member premenne. Tym padom je potom velkost public triedy velmi stabilna napriec verziami kniznice. A ziadnu factory metodu nepotrebujete.
Jojo to je opaque pointer nebo se tomu taky říká PIML. Na Qt už jsme koukal, ale to mi nevyhovuje, protože když potřebuju přidat nové rozhraní a rozhodnu se ten privátní typ podědit např. ještě z třídy Context, tak pak musím do té public třídy přidat všechny metody z toho rozhraní a ty metody budu delegovat na ten privátní typ. To je hrozný. Jako už chápu, proč se všechno dělá v Javě, protože C++ je OOP asi jako Java funkcionální.
-
Pozrel by som sa ako na to ide Qt ktora ma velmi dobru ABI kompatibilitu. Tam ma kazda trieda len jednu privatnu member premennu a to je pointer na instanciu privatnej triedy ktora drzi vsetko potrebne member premenne. Tym padom je potom velkost public triedy velmi stabilna napriec verziami kniznice. A ziadnu factory metodu nepotrebujete.
Taky to jde, má to nevýhodu, že ke všem memberům se přistupuje přes pointer, což je otravné psát a může to stát nějaký výkon. Virtuální funkce mají výhodu v tom, že performance hit je jen při volání metody, ne při přístupu ke každému memberu, ale zase nevýhodu v tom, že se nesmí rozbít vtable. Podle vkusu každého soudruha.
-
Ten přetížený new operátor bude implementovat ta třída z té knihovny.
Takže globální operátor new aplikace se přetíží v nějaké dynamické knihovně? To opravdu není dobrý nápad. Ono to navíc ani pořádně nefunguje: http://stackoverflow.com/questions/1054697/why-isnt-my-new-operator-called
-
Mimochodem když už jsme u těch špinavých hacků, tak to co chceš udělat (alokovat určitou oblast paměti a v ní vytvořit třídu), se dělá pomocí placement new (https://isocpp.org/wiki/faq/dtors#placement-new). Ale pro tvůj účel (zajištění binární kompatibility) to rozhodně nedoporučuju.
-
Pozrel by som sa ako na to ide Qt ktora ma velmi dobru ABI kompatibilitu. Tam ma kazda trieda len jednu privatnu member premennu a to je pointer na instanciu privatnej triedy ktora drzi vsetko potrebne member premenne. Tym padom je potom velkost public triedy velmi stabilna napriec verziami kniznice. A ziadnu factory metodu nepotrebujete.
Ono z toho vychazi i KDE. Tady to maji pekne popsay.https://techbase.kde.org/Policies/Binary_Compatibility_Issues_With_C%2B%2B (https://techbase.kde.org/Policies/Binary_Compatibility_Issues_With_C%2B%2B)
Java ma v tomhle obrovskou vyhodu, ze jeji executable format uklada hash signatury metod. Takze se vam nemuze stat, ze zavolate metodu tridy ktera zabira 60 bajtu na zasobniky, zatimco jeji knihovni metoda si mysli, ze ma 64 bajtu. Mel jsem kolegu, ktery byl schotny tyhle veci vycist z ELF formatu. My obycejni smrtelnici se musime smirit s tim, ze zmeny v ABI mohou vest k nahodnym, tezko resitelnym segfaltum.
Jedna z moznosti jak to obejit, je nedelat zadny knihovny a svechni mit v templatech. Pomoci CRTP se da dosahnout i toho, ze i virtualni fce jsou inline a nepotrabuji zadne RTTI. Vysledky kod, je ale tezko udrzovatelny.
-
Ten přetížený new operátor bude implementovat ta třída z té knihovny.
Takže globální operátor new aplikace se přetíží v nějaké dynamické knihovně? To opravdu není dobrý nápad. Ono to navíc ani pořádně nefunguje: http://stackoverflow.com/questions/1054697/why-isnt-my-new-operator-called
Bude takhle - přeložením hlavní aplikace vznikne mimojiné např. knihovna core.dll, se kterou se budou linkovat pluginy (třeba plugin1.dll). Ta core.dll knihovna by případně mohla přetížit i ten globální new operátor.
Požadavky: exportované třídy z core.dll by měly mít možnost měnit svoje datové členy a hierarchii dědění, instance exportovaných třídy budou vytvářeny jen na haldě, takže dynamickou alokací
Co není potřeba: není potřeba, aby exportovaní třídy z core.dll šly podědit v pluginech, veškerá manipulace s datovými členy tříd bude probíhat přes metody.
-
Bude takhle - přeložením hlavní aplikace vznikne mimojiné např. knihovna core.dll, se kterou se budou linkovat pluginy (třeba plugin1.dll). Ta core.dll knihovna by případně mohla přetížit i ten globální new operátor.
Proč to chceš hackovat přetěžováním operátoru new a nepoužiješ standardní řešení s čistě abstraktní bázovou třidou sloužící jako interface?
-
nam se osvedcilo toto:
class A {
public:
A();
virtual ~A();
void neco();
virtual void necoJineho();
private:
class AImpl *impl;
};
Trik je v tom impl. Struktura je vzdy stejna i kdyz pridavas fieldy, metody apod, protoze je vse ulozene v impl, ktery je neznamej (neni v public headeru). virtualni destruktor mimo jine zajisti virtualni tabulku (nezmeni velikost ani pridanim dalsich metod). Jinak pracujes normalne s "new".
Na projektu jsme meli asi 60 DLL knihoven a patche fungovali velmi dobre i kdyz menili velmi podstatne funkcionalitu.
-
Mimochodem když už jsme u těch špinavých hacků, tak to co chceš udělat (alokovat určitou oblast paměti a v ní vytvořit třídu), se dělá pomocí placement new (https://isocpp.org/wiki/faq/dtors#placement-new). Ale pro tvůj účel (zajištění binární kompatibility) to rozhodně nedoporučuju.
Jo ten placement new by teoreticky mohl pomoct. Ono vlastně to i zřejmě odpovídá na tu moji otázku, to mě nenapadlo, díky :D
Protože to moje
class TridaZKnihovny {
}
void* opeator new(size_t size) { return std::malloc(sizeof(TridaZKnihovny);}
}
a pak v kodu
TridaZKnihovny* instance = new TridaZKnihovny();
muzu nahradit taky jako
class TridaZKnihovny {
static size_t getSize() { return sizeof(TridaZKnihovny); }
}
a v kodu
TridaZKnihovny* instance = new (std::malloc(TridaZKnihovny::getSize()) TridaZKnihovny();
A protoze vysledek by mel byt stejny, tak je defakto zajistene, ze ten parametr size
u opeatoru new
skutecne odpovida ciste jen velikosti te tridy a neni tam zahrnuto nic dalsiho.
-
nam se osvedcilo toto:
class A {
public:
A();
virtual ~A();
void neco();
virtual void necoJineho();
private:
class AImpl *impl;
};
Trik je v tom impl. Struktura je vzdy stejna i kdyz pridavas fieldy, metody apod, protoze je vse ulozene v impl, ktery je neznamej (neni v public headeru). virtualni destruktor mimo jine zajisti virtualni tabulku (nezmeni velikost ani pridanim dalsich metod). Jinak pracujes normalne s "new".
Na projektu jsme meli asi 60 DLL knihoven a patche fungovali velmi dobre i kdyz menili velmi podstatne funkcionalitu.
Jenze prave me vadi to, ze kdyz bych chtel pridat dalsi interface (dalsi bazovou tridu), tak to musim udelat v AImpl a pak vsechny metody toho interfacu navic jeste pridat do te public tridy.
class AImpl {
void udelejNeco();
}
class A {
void udelejNeco() { m_impl->udelejNeco(); }
private:
AImpl* m_impl;
}
a ted si vymyslim dalsi interface,
class IContext {
void neco();
}
tak musim to prechozi zmenit na:
class AImpl : public IContext {
void udelejNeco();
}
class A {
void udelejNeco() { m_impl->udelejNeco(); }
void neco( m_impl->neco();)
private:
AImpl* m_impl;
}
To se mi vubec nelibi tohle, protoze pak budu chtit pridat dalsich 5 interface a budu porad neco takhle kopirovat, no co to je?
-
no co to je?
C++ ...
-
no co to je?
C++ ...
JJ presne, skoda ze C++ nema alias this tak jako jazyk D, to se pak toto implmentuje nadherne
class AImpl {
void udelejNeco();
}
class A {
private AImpl m_impl;
alias m_impl this;
}
-
no co to je?
C++ ...
JJ presne, skoda ze C++ nema alias this tak jako jazyk D, to se pak toto implmentuje nadherne
class AImpl {
void udelejNeco();
}
class A {
private AImpl m_impl;
alias m_impl this;
}
Bohuzel musim pouzivat taky Qt, jinak bych hned skocil po nejakem lepsim jazyce, ktery se podobne jako C++ kompiluje primo do nativniho kodu.
-
no co to je?
C++ ...
JJ presne, skoda ze C++ nema alias this tak jako jazyk D, to se pak toto implmentuje nadherne
class AImpl {
void udelejNeco();
}
class A {
private AImpl m_impl;
alias m_impl this;
}
Bohuzel musim pouzivat taky Qt, jinak bych hned skocil po nejakem lepsim jazyce, ktery se podobne jako C++ kompiluje primo do nativniho kodu.
Jedna z veci co mi obcas na D chybi je binding pro Qt.
-
hehe, kouknu na forum D a vidim, ze zrovna dneska byl jako open-source uvolněn Swift od Applu
-
Tedy tady je odborníků na C++ až se mi chce brečet.
Původně jsem chtěl tazateli poradit, ale neudělám to, nemyslím si, že by mou odpověď našel mezi tou hromadou nesmyslných blábolů
Jinak přetěžování new v C++ běžně používám. Zpravidla ale jako nějakou base třídu, z níchž pak dědím, abych ten způsob alokace dostal všude tam, kde ho potřebuju. Třeba tady
https://github.com/ondra-novak/lightspeed/blob/master/src/lightspeed/base/memory/dynobject.h#L67
-
Jinak přetěžování new v C++ běžně používám. Zpravidla ale jako nějakou base třídu, z níchž pak dědím, abych ten způsob alokace dostal všude tam, kde ho potřebuju. Třeba tady
Tohle je úplně uhozený způsob udržení binární kompatibility, nebude to fungovat. Všichni se to snaží tazateli říct, ale on to nechce pochopit. Pak přijde "odborník" a doporučí podobnou blbost.
Nebude to fungovat třeba z toho důvodu, že default constructor je inline, takže se nevykoná v dynamické libce, ale u klienta. Správná velikost na správnou inicializaci objektu nestačí, musí být splněna celá řada další podmínek.
-
Nebude to fungovat třeba z toho důvodu, že default constructor je inline, takže se nevykoná v dynamické libce, ale u klienta. Správná velikost na správnou inicializaci objektu nestačí, musí být splněna celá řada další podmínek.
Hlavně i kdyby nějak vyřešil ten problém s new, tak pořád tu bude možnost, že uživatel knihovny bude chtít vytvořit instanci té třídy na zásobníku.
-
Tohle je úplně uhozený způsob udržení binární kompatibility, nebude to fungovat. Všichni se to snaží tazateli říct, ale on to nechce pochopit. Pak přijde "odborník" a doporučí podobnou blbost.
Nebude to fungovat třeba z toho důvodu, že default constructor je inline, takže se nevykoná v dynamické libce, ale u klienta. Správná velikost na správnou inicializaci objektu nestačí, musí být splněna celá řada další podmínek.
Já jsem taky neříkal, že odpovím tazateli, diskuze se zvrhla mezi účastníky, kteří netuší, k čemu se přetížený new používá.
No budiž.
Tedy pokud jde o binární kompatibilitu, tam se to takhle vůbec nemůže dělat. Ať zapomene na new, ať vůbec zapomene na to, že by do hlavičkových souborů dával finální třídy. Z knihovny může exportovat jen rozhraní, tedy třídy obsahující __pouze__ abstraktní funkce. A i tady bude mít problém, pokud bude do takového rozhraní přidávat funkce, musí nutně ta rozhraní verzovat.
Úplně první co z knihovny vypadne je nějaké základní rozhraní, které zpravidla obsahuje továrnu pro všechny objekty, které knihovna umí vytvořit.
class IMain {
public:
virtual IFoo *createFoo() = 0;
virtual IBar *createBar(...) = 0;
virtual ~IMain() {}
}
Pokud je třeba, aby knihovna volala nějaké věci z hlavního programu, je jí to třeba dodat také přes rozhraní. Komunikace mezi knihovnou a hlavním program nelze zajistit lépe, než právě takhle. To je jediný čistý způsob jak to udělat.
Navázání komunikace s knihovnou se zpravidla děje přes C-čkovskou funkci (extern "C"). Tohle dobře funguje ve Windows.
Ve Windows je ještě jedna potíž a to s oddělenými heapy knihovny a hlavního programu. Proto není možné objekt vytvořený knihovnou zničit v hlavním programu a naopak. Rozhodně nezapomínat na virtuální destruktor, který tohle řeší.
(Pokud se ve windows použijí DLL runtime knihovny, pak je to trochu lepší, protože všechny alokace patří knihovně MSVCRT.DLL - stačí ale aby jeden modul měl statické linkování a pak jeho alokace patří pouze jemu. - V linuxu je jeden heap, tam to problém není)
Takže shrnutí. Binární kompatibilita jen přes interfacy (a to ještě verzované). Jinak lze využít dynamické knihovny i bez této nutnosti, za předpokladu, že s každou změnou hlavičkových souborů vydám novou verzi dynamické knihovny a budu je verzovat. Ale to není binární kompatibilita. Navíc člověka umlátí zejména v linuxu dependency-hell (ve Windows to řeší DLL-hell a winsxs-hell)
PS: teď mě napadlo, že pokud používá výjimky, taky si užije legrace, až bude výjimku z knihovny chytat v hlavním kódu. Třeba ve windows, když propadá výjimka z hlavního programu do knihovny přes callback a zpět do hlavního programu, stávalo se, že se neodchytla a spadla až do unexpected()
-
Hlavně i kdyby nějak vyřešil ten problém s new, tak pořád tu bude možnost, že uživatel knihovny bude chtít vytvořit instanci té třídy na zásobníku.
Divim se, za tady jeste nepadlo slovo "tovarna". Ja napriklad pouzivam tohle:
https://github.com/tora-tool/tora/blob/master/extlibs/loki-extra/include/loki/Factory_alt.h (https://github.com/tora-tool/tora/blob/master/extlibs/loki-extra/include/loki/Factory_alt.h)
Pouziva se to napr. takhle
std::auto_ptr <SQLParser::Statement> stat = StatementFactTwoParmSing::Instance().create("OracleDML", m_lastText, "");
SQLParser::Statement je zakladni trida, jejiz header includuju. Zadny dalsi headery nepotrebuju. Headery implementaci me nezajimaji.
Tovarna mi vytvori pointer na tridu, ktera se zaregistrovala se jmenem "OracleDML".
Tovarna je deklarovana jako:
typedef TORA_EXPORT Util::GenericFactory<SQLParser::Statement, LOKI_TYPELIST_2(const QString &, const QString&)> StatementFactTwoParm;
class TORA_EXPORT StatementFactTwoParmSing: public ::Loki::SingletonHolder<StatementFactTwoParm> {};
V C++ existuji i jine zpusoby ja zakazat alokaci na zasobniku.
-
budu mit třídu exportovanou dynamickou knihovnou, např. class TridaZKnihovny.
Pokud v programu který tuto knihovnu používá vytvořím proměnnou
TridaZKnihovny* instance = new TridaZKnihovny();
tak mi kompilátor vygeneruje kód závislý na velikosti, kterou odvodí třeba z hlavičkového souboru.
Takže když bych updatoval tu dyn. knihovnu, tak mi to může rozbít ten program, protože se velikost toho
typu může změnit.
Teď ta otázka: Potřeboval bych teda zaručit, že new alokouje správnou velikost. V C++ můžete přetížit operátor new.
Děláš to blbě. Správné řešení je zavolat ne-oop funkci v knihovně, ta vytvoří instanci podle toho jak potřebuje a z knihovny si následně vyžádat rozhraní alias tabulka pointerů na metody. Každá nová funkcionalita má svoji novou tabulku.
Jedna z možných implementací je technologie COM a QueryInterface.
-
Nebude to fungovat třeba z toho důvodu, že default constructor je inline, takže se nevykoná v dynamické libce, ale u klienta. Správná velikost na správnou inicializaci objektu nestačí, musí být splněna celá řada další podmínek.
Hlavně i kdyby nějak vyřešil ten problém s new, tak pořád tu bude možnost, že uživatel knihovny bude chtít vytvořit instanci té třídy na zásobníku.
To jsem mel poresene. V debug nastaveni (tzn. pri vyvoji) je new operator a konstruktory privatni, takze na stacku nikdo nic nevytvori, stejne tak nepouzije normalni new, ale musi pres makro, ktere se nahrazuje volanim metody ve tride, ktera je jako friend. No pridam cely ten kod. Na C++ nejsem odbornik, takze jsem nejake veci nemel domyslene. Hlavne to, ze jsem myslel, ze volani virtualni metody tridy z knihovny jde pres tabulku importu, jenze on ten kompilator generuje kod i tak pres pointer na tabulku virtualnich metod, takze to co jsem chtel celkove udelat stejne nebude fungovat.
#ifdef QT_DEBUG
#define DEBUG
#endif
#ifdef DEBUG
#define CUSTOM_NEW
#define core private
#else
#define core public
#endif
#ifdef CUSTOM_NEW
#define NEW(TYPE) Memory::nnew<TYPE>
#define DEL(obj) Memory::ndel(obj)
#else
#define NEW(Type) new /*(std::malloc(Type::getSize()))*/ Type
#define DEL(obj) delete obj
#endif
#define SIZE_HANDLER_NAME _n_core_get_size
#define CORE_START(Class) \
SIZE_HANDLER(Class) \
core: \
MEM_HANDLERS(Class) \
#define CORE_END \
private:
#define SIZE_HANDLER(Class) \
static size_t SIZE_HANDLER_NAME() { return sizeof(Class); }
#define MEM_HANDLERS(Class) \
void* operator new (size_t size) { /*std::cout << "new called" << std::endl;*/ return std::malloc(Class::SIZE_HANDLER_NAME()); } \
void* operator new (size_t size, void* placement) { /*std::cout << "new placement called" << std::endl;*/ return placement; } \
void operator delete (void* mem) { std::free(mem); } \
void operator delete[] (void* mem) { std::free(mem); } \
friend class Memory;
class Base {
CORE_START(Base)
Base(int i, int j, int k, int l) { m_i = i; m_j = j; m_j = j; m_k = k; }
CORE_END
public:
int m_i;
int m_j;
int m_k;
int m_l;
};
class Memory {
public:
template <typename T, typename... Args>
static /* typename std::enable_if<std::is_base_of<HeapObject, T>::value, T*>::type*/ T* nnew(Args... args) {
return new T(args...);
}
static void ndel(void* obj) {
delete obj;
}
};
#define ITER 10000000
int main(int argc, char* argv[]) {
QElapsedTimer timer;
timer.start();
for (int i = 0; i < ITER; i++) {
auto b = NEW(Base)(10, 20, 50, 70);
DEL(b);
}
std::cout << "TIME: " << timer.elapsed() << std::endl;
return 0;
}
-
Jinak přetěžování new v C++ běžně používám. Zpravidla ale jako nějakou base třídu, z níchž pak dědím, abych ten způsob alokace dostal všude tam, kde ho potřebuju. Třeba tady
Tohle je úplně uhozený způsob udržení binární kompatibility, nebude to fungovat. Všichni se to snaží tazateli říct, ale on to nechce pochopit. Pak přijde "odborník" a doporučí podobnou blbost.
Nebude to fungovat třeba z toho důvodu, že default constructor je inline, takže se nevykoná v dynamické libce, ale u klienta. Správná velikost na správnou inicializaci objektu nestačí, musí být splněna celá řada další podmínek.
Ale ja to chci pochopit, naopak. Akorat jsem nerekl vsechny detaily, treba ze se obejdu bez toho, aby ty tridy z jadra sly v pluginech subclassovat apod.. Proste mel jsem to vymyslene tak, ze jsem byl opravdu presvedceny, ze by to fungovalo a mohl bych se vyvarovat tomu vzoru opaque pointer/pimpl (pokud budu v pohode, kdyz nektere veci nepujdou).
-
Vyjimky pouzivat nehodlam, to uz jsem zachytil, ze v C++ je to resene nejak blbe.
Jinak ja tady popisu, pokusim se co nejsrozumetelneji (vyjadrovani zrovna neni moje silna stranka v posledni dobe), ceho chci vlastne dosahnout.
Takze:
Bude aplikace a vystupem prekladu bude krome te vlastni aplikace taka rada dynamickych knihoven, se kterymi se budou linkovat zasuvne moduly (take ve forme dynamickych knihoven) do aplikace. Proste klasika, kdyz chcete mit aplikaci, do ktere je mozne vytvaret nejake pluginy.
Jak ale zajistim, abych mel co nejprijemnejsi (hlavne flexibilita zmen, coz je problem, kdyz nesmis rozbit tu kompatibilitu) zpusob vyvoje toho jadra?
Chtel bych mit moznost pouzivat java-like interface. Vim, ze v C++ takova moznost neni a muzu vytvorit jen C++ tridu s ciste virtualnima metodama.
V aplikaci bude spousta modelu, kdy je vzdy jedna bazova trida, ze ktere ostatni dedi a navic jeste pridavaji radu interfacu. Typickym prikladem AST strom, kdy je nejaka bazova trida Node a pak subclassy jako ExpressionNode, VariableNode,... a tyhle tridy navic jeste krome tridy Node dedi ruzne interfacy jako IContext atd.. Tech interfacu trida muze dedit treba 10.
Chci mit neco jako operator instanceof v Jave. C++ ma dynamic cast a Qt qobject_cast, ale to mi nevyhovuje, protoze dynamic cast je pomalej a qt zase neumoznuje castnout interface zpatky na tu implementujici tridu.
Mel bych moct delat veci jako
Node* node = ...
if (IContext* ctx = my_cast(node, IContext) { ... }
ALE chci taky moct castit z rozhrani zpet (to uz je v Qt problem, nejde to a dynamic_cast je pomalej a celkove nevim jeho dalsi nevyhody a jestli to vubec jde)
if (VariableNode* var = my_cast(ctx, VariableNode) { .... }
To casteni uz mam vyresene, alespon dokud se neobjevi dalsi problemy.
Ted jde o to, jak teda pojmout ten vyvoj trid jadra, abych tam mohl provadet zmeny jako pridavani i virtualnich metod do libovolne tridy v hierarchii dedeni, pridavani dalsich trid do hierarchie dedeni apod.. Co uz jsem pochopil, hlavne z toho odkazu co tu daval Gamer na to KDE (diky, super odkaz), tak tohle je proste problem a nejde to. TAKZE CO TED, mam se na to vykaslat? To asi nepujde, musim to nejak vymyslet, ale zase aby to nebylo brutalne neefektivni, protoze to uz bych mohl to delat v Jave (ba ne nemuzu, potrebuju pouzivat Qt).
Proc jsem se tady na zacatku ptal na ten operator new bylo protoze jsem se domnival, ze ten problem je hlavne v tom, ze kompilator nesmi v pluginu generovat kod, co je zavisli na velikosti tech trid z jadra, takze jsem se snazil vymyslet, jak tu velikost zjistit az za behu a podle toho alokovat dostatecny prostor. BOHUZEL mi pak doslo, ze problem je i v tom pridavani virtualnich metod, pridavani trid do hierarchie apod.. Tohle jsem myslel, ze bude ok, protoze jsem pocital s tim, ze zadny datovy cleny nebudu pouzivat primo, ale pres metody a naivne jsem se domnival, ze volani metod jde prece pres tabulku importu (o pointerech do TVM jsem vedel, ale myslel jsem i tak, ze ten kod pouzivajici ty pointery proste bude v te knihovne a ne v pluginu).
Jinak vazim si vasich postrehu, diky za ne.
-
Úplně první co z knihovny vypadne je nějaké základní rozhraní, které zpravidla obsahuje továrnu pro všechny objekty, které knihovna umí vytvořit.
class IMain {
public:
virtual IFoo *createFoo() = 0;
virtual IBar *createBar(...) = 0;
virtual ~IMain() {}
}
A kdyz ted v moji aplikaci bude IFoo interface k nejake implementaci FooImpl, ktera bude dedit ze tridy BarImpl, k niz bude to rozhrani IBar, tak by to rozhrani IFoo melo dedit z rozhrani IBar, jenze to uz jsem zase v situaci, kdy pak nemuzu do rozhrani IBar pridat nejakou virtualni metodu. aniz bych nerozbil kod co pouziva IFoo (co jsem pochopil, tak leaf classam muzu virtualni metody pridavat na konec - krome windows, ten to pry nejak muze preorganizovat). Resenim by bylo zkopirovat ty metody z rozhrani IBar do IFoo, ale jako tohle fakt delat nechci. Chapu to spravne?
-
Člověče máš to nějaký složitý, a je v tom mnoho věcí naráz. Nicméně k čemu se umím vyjádřit je pomalost dynamic_castu. Ano, ten je pomalý, ale ne proto, že by ti programátoři to neuměli napsat, ale ono to prostě líp nejde. dynamic_cast musí na základě dosti omezených dat o objektu a požadované třídě najít adresu patřičného potomka a to s ohledem na různá vícenásobná a hlavně virtuální dědění (což je samozřejmě chuťovka). Navíc třeba ve Windows to řeší i přes různé knihovny, kdy každá knihovna může být přeložena jiným překladačem, takže neexistuje jedný způsob, jak identifikovat třídu, takže se používá zamanglované jméno a k porovnávání se používá funkce strcmp.
Tohle jsem řešil taky s ruznými způsoby a stupni úspěchu a neúspěchu. A nakonec jsem skončil u řešení, které nazývám "IInterface". Je to třída, která poskytuje funkci getInterface (zkráceně getIfc) a zapisuje se
Potomek &p = predek.getIfc<Potomek>()
nebo
Potomek *p = predek.getIfcPtr<Potomek>()
Ten vztah tak nemusí být jen potomek předek, ale lze takhle získat pointer na libovolné rozhraní, které objekt může přímo implementovat nebo i nepřímo. Třeba mám rozhranní IHttpRequest, který také poskytuje ITCPConnection a dokonce IJobManager právě prostřednictvím getIfcPtr. Přestože IJobManager nemá nic společného s requestem, přesto to funguje jako service, kterou request nabízí svému handleru (handler si může spustit job)
Implementace tohoto prostředku mám ve své knihovně lightspeed
https://github.com/ondra-novak/lightspeed/blob/master/src/lightspeed/base/interface.h
https://github.com/ondra-novak/lightspeed/blob/master/src/lightspeed/base/interface.tcc
Soubor *.tcc se inkluduje do každého *.cpp, kde se to používá, je to implementační část šablony.
Kupodivu to nefunguje automaticky tak jak by sis představoval. Když totiž nic neuděláš, rozhraní vše realizuje přes dynamic_cast, takže žádná věda. Ale ušetří to spoustu práce, tam, kde rychlost nepotřebuješ řešit. Pokud chceš ale nabízet nějaké zkratky, nebo extrabuřty, stačí, když potomek implementuje funkci
virtual void *proxyInterface(IInterfaceRequest &p)
Funkce si z p vyzvedne typ třídy (jako typeid) kterou chce volající získat a pokud ji umí, pak vrátí buď sebe přes static_cast na požadovanou třídu nebo interface - musí ho pak přetypovat na void *, nebo přímo vrátí pointer na službu, která požadovanou třídu implementuje. Je akorát třeba pamatovat na to, že pointer musí být před vrácením nejprve přetypován na požadovaný typ přes static_cast, než je převeden na void *, jelikož šablona si to na druhé straně zase přetypuje zpět na požadovanou třídu.
Funguje to v celku dobře, pokud mám někde místo, které dost často dělá přetypování a chci se vyhnout dynamic_castu, tak v potomkovi definuju přímo onu zkratku a tím se vyhnu pomalému dynamic_cast. Je to opravdu rychlejší, protože ve výsledku to je jen jedno volání virtuální funkce, jedna operace porovnávání a static_cast nic nestojí.
Samozřejmě, jakmile se mi těch tříd co nabízím začne množit víc, začne být ten rozhodovací mechanismus složitejší a pak je třeba si spočítat, jestli náhodou ten dynamic_cast nevyjde rychlejší. Potom stačí pouze zavolat výchozí implementaci IInterface a on už to zařídí sám.
-
Vyjimky pouzivat nehodlam, to uz jsem zachytil, ze v C++ je to resene nejak blbe.
Jak blbě?
Chtel bych mit moznost pouzivat java-like interface. Vim, ze v C++ takova moznost neni a muzu vytvorit jen C++ tridu s ciste virtualnima metodama.
Třída s čistě virtuálními metodami je v C++ to samé, jak v Javě interface. C++ má vícenásobnou dědičnost, nepotřebuje speciální interfacy.
Chci mit neco jako operator instanceof v Jave. C++ ma dynamic cast a Qt qobject_cast, ale to mi nevyhovuje, protoze dynamic cast je pomalej a qt zase neumoznuje castnout interface zpatky na tu implementujici tridu.
To říká kdo, že je dynamic_cast pomalejší oproti instanceof? V GCC a Clangu je implementovaný prakticky totožně jako v JVM.
dynamic_cast je pomalý, ale to je i instanceof.
dynamic_cast je pomalej a celkove nevim jeho dalsi nevyhody a jestli to vubec jde
Jde to, to je právě jeho účel. Pomalý je úplně stejně jako přetypovávání v Javě.
Ted jde o to, jak teda pojmout ten vyvoj trid jadra, abych tam mohl provadet zmeny jako pridavani i virtualnich metod do libovolne tridy v hierarchii dedeni, pridavani dalsich trid do hierarchie dedeni apod.. Co uz jsem pochopil, hlavne z toho odkazu co tu daval Gamer na to KDE (diky, super odkaz), tak tohle je proste problem a nejde to. TAKZE CO TED, mam se na to vykaslat? To asi nepujde, musim to nejak vymyslet, ale zase aby to nebylo brutalne neefektivni, protoze to uz bych mohl to delat v Jave (ba ne nemuzu, potrebuju pouzivat Qt).
C++ má na tohle trik: virtuální dědičnost. Pokud nezměníte pořadí již definovaných metod v rozhraních, pak klidně můžete přidávat další. (Alespoň v implementaci GCC + Clang.)
-
A kdyz ted v moji aplikaci bude IFoo interface k nejake implementaci FooImpl, ktera bude dedit ze tridy BarImpl, k niz bude to rozhrani IBar, tak by to rozhrani IFoo melo dedit z rozhrani IBar, jenze to uz jsem zase v situaci, kdy pak nemuzu do rozhrani IBar pridat nejakou virtualni metodu. aniz bych nerozbil kod co pouziva IFoo (co jsem pochopil, tak leaf classam muzu virtualni metody pridavat na konec - krome windows, ten to pry nejak muze preorganizovat). Resenim by bylo zkopirovat ty metody z rozhrani IBar do IFoo, ale jako tohle fakt delat nechci. Chapu to spravne?
Můžeš použít moje řešení IInterface (viz výše) ale samozřejmě musíš na to myslet úplně na začátku, kdy každé rozhraní dědí IInterface. To pak každé rozhraní poskytuje funkci getIfc<typ> a každý ten BarImpl nebo FooImpl může na základě požadavku vrátit pointer na rozhraní, které požaduješ. Je to hodně podobné tomu, co dělá ve windows QueryInterface. Akorát QueryInterface vyžaduje transitivitu a reflexivitu, což moje řešení ne. QueryInterface je blíže spíš dynamic_castu, zatímco mé řešení spíš připomíná funkci QueryService, které zahrnuji i QueryInterface.
Pak můžeš napsat
IFoo *foo = main->createFoo();
IBar *p = foo->getIfc<IBar>()
Je v2c9 FooImpl, aby si zařídilo převod z IFoo na IBar. Třeba tak, že bude třída FooBar, která dědí BarImpl a FooImpl
-
To jsem mel poresene. V debug nastaveni (tzn. pri vyvoji) je new operator a konstruktory privatni, takze na stacku nikdo nic nevytvori, stejne tak nepouzije normalni new, ale musi pres makro, ktere se nahrazuje volanim metody ve tride, ktera je jako friend.
Hmm, ta knihovna teda ty konstruktory sice bude exportovat, ale přesto by neměly být používány. Takže uživatel dostane speciální makro, které bude při vývoji hlídat, zda to náhodou neudělal. No, pominu-li, že mi to přijde ulítlé, tak jen dodám, aby sis dal pozor na implicitní konstruktory, protože jejich volání jsi nezabránil.
-
Vyjimky pouzivat nehodlam, to uz jsem zachytil, ze v C++ je to resene nejak blbe.
Jak blbě?
Chtel bych mit moznost pouzivat java-like interface. Vim, ze v C++ takova moznost neni a muzu vytvorit jen C++ tridu s ciste virtualnima metodama.
Třída s čistě virtuálními metodami je v C++ to samé, jak v Javě interface. C++ má vícenásobnou dědičnost, nepotřebuje speciální interfacy.
Chci mit neco jako operator instanceof v Jave. C++ ma dynamic cast a Qt qobject_cast, ale to mi nevyhovuje, protoze dynamic cast je pomalej a qt zase neumoznuje castnout interface zpatky na tu implementujici tridu.
To říká kdo, že je dynamic_cast pomalejší oproti instanceof? V GCC a Clangu je implementovaný prakticky totožně jako v JVM.
dynamic_cast je pomalý, ale to je i instanceof.
dynamic_cast je pomalej a celkove nevim jeho dalsi nevyhody a jestli to vubec jde
Jde to, to je právě jeho účel. Pomalý je úplně stejně jako přetypovávání v Javě.
Ted jde o to, jak teda pojmout ten vyvoj trid jadra, abych tam mohl provadet zmeny jako pridavani i virtualnich metod do libovolne tridy v hierarchii dedeni, pridavani dalsich trid do hierarchie dedeni apod.. Co uz jsem pochopil, hlavne z toho odkazu co tu daval Gamer na to KDE (diky, super odkaz), tak tohle je proste problem a nejde to. TAKZE CO TED, mam se na to vykaslat? To asi nepujde, musim to nejak vymyslet, ale zase aby to nebylo brutalne neefektivni, protoze to uz bych mohl to delat v Jave (ba ne nemuzu, potrebuju pouzivat Qt).
C++ má na tohle trik: virtuální dědičnost. Pokud nezměníte pořadí již definovaných metod v rozhraních, pak klidně můžete přidávat další. (Alespoň v implementaci GCC + Clang.)
#############################
Vyjimky pouzivat nehodlam, to uz jsem zachytil, ze v C++ je to resene nejak blbe.
Jak blbě?
Myslim, ze jsem je zacal brat jako blbe reseni po precteni tohohle clanku http://250bpm.com/blog:4 (http://250bpm.com/blog:4). Celkove jsem na to ale narazil
uz x-krat, ze jsou proste resene spatne. Konkretne proc si nepamatuju, v C++ jsem zatim skoro nic nenaprogramoval. Mam jen nejake ty teoreticke znalosti z toho co jsem si procital v prubehu zivota, abych nebyl omezenej jen na ty moderni jazyky jako Java.
Chci mit neco jako operator instanceof v Jave. C++ ma dynamic cast a Qt qobject_cast, ale to mi nevyhovuje, protoze dynamic cast je pomalej a qt zase neumoznuje castnout interface zpatky na tu implementujici tridu.
To říká kdo, že je dynamic_cast pomalejší oproti instanceof? V GCC a Clangu je implementovaný prakticky totožně jako v JVM.
dynamic_cast je pomalý, ale to je i instanceof.
To nerikam, ze je pomalejsi nez instanceof, ale kdyz uz neco delam v tom C++, tak bych chtel, aby to bylo efektivnejsi nez treba v Jave.
Na tu virtualni dedicnost kouknnu, ale taky jsem uz na netu narazil na to, ze je to nejaky pomalejsi ta virtualni dedicnost.
-
To jsem mel poresene. V debug nastaveni (tzn. pri vyvoji) je new operator a konstruktory privatni, takze na stacku nikdo nic nevytvori, stejne tak nepouzije normalni new, ale musi pres makro, ktere se nahrazuje volanim metody ve tride, ktera je jako friend.
Hmm, ta knihovna teda ty konstruktory sice bude exportovat, ale přesto by neměly být používány. Takže uživatel dostane speciální makro, které bude při vývoji hlídat, zda to náhodou neudělal. No, pominu-li, že mi to přijde ulítlé, tak jen dodám, aby sis dal pozor na implicitní konstruktory, protože jejich volání jsi nezabránil.
Oni budou pozivane, ale az pri release, pak se to makro totiz preklada na normalni new a ty konstruktory uz nejsou privatni. Takhle tam v release nebude ten overhead spojeny s tim volanim metod na te Memory tride. Proste kdyz programator bude vyvijet v debugu a pak az bude vsechno odladene to jen prelozi jako release, tak to bude v pohode. V debugu to nepovoli ani pouzivat primo new, ani alokovat na stacku. Ale byl to jen nastrel, hodne veci tam urcite jeste neni poresene a jak uz jsem pochopil, tak to stejne bude k nicemu.
-
V debugu to nepovoli ani pouzivat primo new, ani alokovat na stacku.
Jenže aktuálně ti to alokaci na stacku pořád umožní například přes implicitní copy kontruktor.
-
Tohle jsem řešil taky s ruznými způsoby a stupni úspěchu a neúspěchu. A nakonec jsem skončil u řešení, které nazývám "IInterface". Je to třída, která poskytuje funkci getInterface (zkráceně getIfc) a zapisuje se
Potomek &p = predek.getIfc<Potomek>()
nebo
Potomek *p = predek.getIfcPtr<Potomek>()
Ten vztah tak nemusí být jen potomek předek, ale lze takhle získat pointer na libovolné rozhraní, které objekt může přímo implementovat nebo i nepřímo. Třeba mám rozhranní IHttpRequest, který také poskytuje ITCPConnection a dokonce IJobManager právě prostřednictvím getIfcPtr. Přestože IJobManager nemá nic společného s requestem, přesto to funguje jako service, kterou request nabízí svému handleru (handler si může spustit job)
Implementace tohoto prostředku mám ve své knihovně lightspeed
https://github.com/ondra-novak/lightspeed/blob/master/src/lightspeed/base/interface.h
https://github.com/ondra-novak/lightspeed/blob/master/src/lightspeed/base/interface.tcc
Díky, trochu se tim prostouram a podivam se, jaky pristup by se mi mohl hodit.
-
V debugu to nepovoli ani pouzivat primo new, ani alokovat na stacku.
Jenže aktuálně ti to alokaci na stacku pořád umožní například přes implicitní copy kontruktor.
To tam jeste chybi no, takze do makra CORE_START pridam implicitni copy konstruktor jako private. Stejne ty objekty nebudu nikdy kopirovat pres copy konstruktory.
Stejne to nechapu, ze se ten C++ model nevytvoril nebo nejak pozdeji nepredelal tak, aby tam nebyly ty problemy jako ze clovek nemuze pridat dalsi virtualni metodu do tridy ze ktere uz jina dedi apod.. Kdyz nad tim premyslim, tak by stacilo to udelat nejak tak, aby kdyz kompilator generuje kod co pouziva tridu z knihovny, tak nepouzival ty ukazatele do virtualnich tabulek primo, ale nechal to resit nejak tu tridu. Chtelo by to nejaky jiny jazyk, co se taky preklada do nativniho kodu a nevnucuje pouziti GC, ale je udelan lepe.
-
A kdyz ted v moji aplikaci bude IFoo interface k nejake implementaci FooImpl, ktera bude dedit ze tridy BarImpl, k niz bude to rozhrani IBar, tak by to rozhrani IFoo melo dedit z rozhrani IBar, jenze to uz jsem zase v situaci, kdy pak nemuzu do rozhrani IBar pridat nejakou virtualni metodu. aniz bych nerozbil kod co pouziva IFoo (co jsem pochopil, tak leaf classam muzu virtualni metody pridavat na konec - krome windows, ten to pry nejak muze preorganizovat). Resenim by bylo zkopirovat ty metody z rozhrani IBar do IFoo, ale jako tohle fakt delat nechci. Chapu to spravne?
Můžeš použít moje řešení IInterface (viz výše) ale samozřejmě musíš na to myslet úplně na začátku, kdy každé rozhraní dědí IInterface. To pak každé rozhraní poskytuje funkci getIfc<typ> a každý ten BarImpl nebo FooImpl může na základě požadavku vrátit pointer na rozhraní, které požaduješ. Je to hodně podobné tomu, co dělá ve windows QueryInterface. Akorát QueryInterface vyžaduje transitivitu a reflexivitu, což moje řešení ne. QueryInterface je blíže spíš dynamic_castu, zatímco mé řešení spíš připomíná funkci QueryService, které zahrnuji i QueryInterface.
Pak můžeš napsat
IFoo *foo = main->createFoo();
IBar *p = foo->getIfc<IBar>()
Je v2c9 FooImpl, aby si zařídilo převod z IFoo na IBar. Třeba tak, že bude třída FooBar, která dědí BarImpl a FooImpl
Me se na tom nelibi to, ze i kdyz vim, ze IFoo by melo poskytovat stejne metody co IBar, kdyz FooImpl dedi z BarImpl, tak stejne musim z IFoo nejakym zpusobem tahat to rozhrani IBar a nemuzu ty metody toho IBar rozhrani volat primo pres IFoo. Kdyz bych udelal to rozhrani IFooBar, tak bych ho zase musel tim ctrl+c ctrl+v zpusobem udrzovat konzistentni s IFoo a IBar.
Zkusim zapremyslet nad tim, jestli by se pri dodrzovani nejake konvence nevyplatilo udelat nejaky preprocesor kodu a plugin do nejakeho IDE, ktery by mi umoznoval tyhle veci nejak automatizovat, takze bych treba vytvoril tridu IBar a pak IFoo a na misto dedeni bych pouzil nejake prazdne makro treba class IFoo EXTENDS(IBar) a IDE by podle toho poznalo, ze mi ma intelli-sense pri pouziti ifoo-> nabizet i metody z IBar a preprocesor by zase vedel, ze pri prekladu ma vsechny metody z IBar zkopirovat do IFoo.
-
Na Qt už jsme koukal, ale to mi nevyhovuje, protože když potřebuju přidat nové rozhraní a rozhodnu se ten privátní typ podědit např. ještě z třídy Context, tak pak musím do té public třídy přidat všechny metody z toho rozhraní a ty metody budu delegovat na ten privátní typ. To je hrozný. Jako už chápu, proč se všechno dělá v Javě, protože C++ je OOP asi jako Java funkcionální.
Pokud by člověku nevadil dynamic_cast, tak by se tomu kopírování šlo vyhnout. Něco na způsob (je to jen nápad):
Privátní třídy:
class base_private
{
public:
virtual ~base_private() {}
};
class A_private : public virtual base_private
{
private:
int a,b;
public:
virtual void metoda1();
virtual void metoda2();
};
class B_private : public virtual base_private
{
private:
int y,x;
public:
virtual void metoda3();
};
class C_private : public A_private, public B_private
{
};
Veřejné rozhraní:
class base
{
protected:
base_private *implementation;
base(base_private *implementation) : implementation(implementation) {}
~base() { delete implementation; }
};
class A : public virtual base
{
public:
A();
void metoda1();
void metoda2();
};
class B : public virtual base
{
public:
B();
void metoda3();
};
class C : public A, public B
{
public:
C();
};
Implementace do knihovny:
A::A() : base(new A_private())
{
}
void A::metoda1()
{
(dynamic_cast<A_private*> (implementation))->metoda1();
}
void A::metoda2()
{
(dynamic_cast<A_private*> (implementation))->metoda2();
}
B::B() : base(new B_private())
{
}
void B::metoda3()
{
(dynamic_cast<B_private*> (implementation))->metoda3();
}
C::C() : base(new C_private())
{
}
Uživatel pak bude moci používat třídy přímočaře:
void use()
{
C *c = new C();
c->metoda3();
}
-
Myslim, ze jsem je zacal brat jako blbe reseni po precteni tohohle clanku http://250bpm.com/blog:4 (http://250bpm.com/blog:4). Celkove jsem na to ale narazil
uz x-krat, ze jsou proste resene spatne. Konkretne proc si nepamatuju, v C++ jsem zatim skoro nic nenaprogramoval. Mam jen nejake ty teoreticke znalosti z toho co jsem si procital v prubehu zivota, abych nebyl omezenej jen na ty moderni jazyky jako Java.
Tohle je obecná kritika systému výjimek, ne toho, jak to dělá C++ (ostatně je to tam i napsané). A IMO je to spíš založené na tom, jak on výjimky používá, než že by to byl problém samotného systému. V ukázkách kombinuje vyhazování výjimek a jejich handlování v rámci jedné funkce, kde bude přístup C vždy mnohem lepší (a taky se používá, viz iostreamy v C++), případně kdy chytá výjimky až v main, což je dobré tak akorát na zalogování před pádem (co tam chcete zachraňovat, když se vám vše zdestruovalo?). Ve chvíli, kdy chyba bude probublávat přes pět vnořených funkcí, už to taková sranda neustále volat if (failed) return error; v každé z nich nebude, a když se odchytí v šesté hned po volání, tak to bude úplně stejně čitelné jako ten if (failed) handle_exception();.
Udržovat konzistentní stav není nijak složité, pokud se držíte RAII a SOLID (hlavně S — Single Responsibility), tím se vyhnete problémům se semi-initialized stavy, protože kompilátor za vás vyřeší správná volání destruktorů při výjimce v konstruktoru.
To nerikam, ze je pomalejsi nez instanceof, ale kdyz uz neco delam v tom C++, tak bych chtel, aby to bylo efektivnejsi nez treba v Jave.
Java sama není nějak pomalá či neefektivní, JVM umí JIT a AOT kompilace (GCC a Art ji umí dokonce zkompilovat do nativní binárky) a běh je hodně rychlý. Pomalé je hlavně to, že se jednoduché věci v Javě často implementují padesáti abstraktními rozhraními, které se potom virtuálně provolávají. Ale to by bylo úplně stejně pomalé, kdyby se to dělalo i v C++.
-
To nerikam, ze je pomalejsi nez instanceof, ale kdyz uz neco delam v tom C++, tak bych chtel, aby to bylo efektivnejsi nez treba v Jave.
Java sama není nějak pomalá či neefektivní, JVM umí JIT a AOT kompilace (GCC a Art ji umí dokonce zkompilovat do nativní binárky) a běh je hodně rychlý. Pomalé je hlavně to, že se jednoduché věci v Javě často implementují padesáti abstraktními rozhraními, které se potom virtuálně provolávají. Ale to by bylo úplně stejně pomalé, kdyby se to dělalo i v C++.
Ja prave budu potrebovat hodne volat nativni metody (potrbuju pouzivat OpenGL) a nelibi se mi ten overhead spojeny s prechodem z Java kontextu do JNI kontextu. Koukal jsem na to, kolik tam je kodu, co se provadi pokazde, kdyz se vola nativni metoda a proto nebudu o te Jave uvazovat, protoze to chci mit co nejefektivnejsi. Jinak bych se na C++ vykaslal a delal to v te Jave.
-
Na Qt už jsme koukal, ale to mi nevyhovuje, protože když potřebuju přidat nové rozhraní a rozhodnu se ten privátní typ podědit např. ještě z třídy Context, tak pak musím do té public třídy přidat všechny metody z toho rozhraní a ty metody budu delegovat na ten privátní typ. To je hrozný. Jako už chápu, proč se všechno dělá v Javě, protože C++ je OOP asi jako Java funkcionální.
Pokud by člověku nevadil dynamic_cast, tak by se tomu kopírování šlo vyhnout. Něco na způsob (je to jen nápad):
.....
Me se na tom nelibi prave uz to kopirovani tech metod, ze trida A musi mit defakto stejne rozhrani jako APrivate a vsechna volani jenom delegovat na tu privatni tridu. Je to vlastne i docela zhorseni vykonnosti, protoze se zdvojnasobi pocet volani metod.
-
Ja prave budu potrebovat hodne volat nativni metody (potrbuju pouzivat OpenGL) a nelibi se mi ten overhead spojeny s prechodem z Java kontextu do JNI kontextu. Koukal jsem na to, kolik tam je kodu, co se provadi pokazde, kdyz se vola nativni metoda a proto nebudu o te Jave uvazovat, protoze to chci mit co nejefektivnejsi. Jinak bych se na C++ vykaslal a delal to v te Jave.
Pokud ti jde až o takovouhle efektivitu, tak bych se osobně zamyslel nad statickým linkováním. Pokud to jenom trochu jde, tak to řeší spoustu problémů.
-
Me se na tom nelibi prave uz to kopirovani tech metod, ze trida A musi mit defakto stejne rozhrani jako APrivate a vsechna volani jenom delegovat na tu privatni tridu. Je to vlastne i docela zhorseni vykonnosti, protoze se zdvojnasobi pocet volani metod.
Ale zase ti to umožní přidat do třídy A metody, aníž by to narušilo ABI. Jak totiž plánuješ řešit například situaci, že modul byl přeložen vůči tomuto rozhraní:
class A
{
public:
virtual void metoda1();
};
class B : public A
{
public:
virtual void metoda3();
};
Ale knihovna je novější a používá už takovéto nové rozhraní:
class A
{
public:
virtual void metoda1();
virtual void metoda2();
};
class B : public A
{
public:
virtual void metoda3();
};
Protože pokud ten modul obsahuje například kód (new B)->metoda3(), tak při použití nové verze knihovny ti to zavolá A::metoda2(). Ona možná rozdílná velikost instance totiž nebude jediný problém.
-
Me se na tom nelibi prave uz to kopirovani tech metod, ze trida A musi mit defakto stejne rozhrani jako APrivate a vsechna volani jenom delegovat na tu privatni tridu. Je to vlastne i docela zhorseni vykonnosti, protoze se zdvojnasobi pocet volani metod.
Ale zase ti to umožní přidat do třídy A metody, aníž by to narušilo ABI. Jak totiž plánuješ řešit například situaci, že modul byl přeložen vůči tomuto rozhraní:
class A
{
public:
virtual void metoda1();
};
class B : public A
{
public:
virtual void metoda3();
};
Ale knihovna je novější a používá už takovéto nové rozhraní:
class A
{
public:
virtual void metoda1();
virtual void metoda2();
};
class B : public A
{
public:
virtual void metoda3();
};
Protože pokud ten modul obsahuje například kód (new B)->metoda3(), tak při použití nové verze knihovny ti to zavolá A::metoda2(). Ona možná rozdílná velikost instance totiž nebude jediný problém.
Jo ja vim, jsem to psal, ze jsem si pak uvedomil, ze volani tech virtualnich metod nejde pres tabulku importu, jak se bezne volaji nevirtualni metody, kdyz se pouziva funkce z dynamicke knihovny, ale ze se pouzivaji i tak ty pointery do TVM (jsem myslel, ze se pouzivaji nejak az v tom kodu te tridy v knihovne, ze se to volani pres tu tabulku importu nejak deleguje na ty TVM pointery a tak ty pointery do TVM v kodu pluginu nemusim resit).
V tom tvem pripade by to pak zvolilo spatny index do TVM no. Se prave snazim vymyslet, jak tohle resit jinak, nez podobnym zpusobem, jak si ukazoval. Ono to ale asi bez toho zdvojnasobeni poctu volani metod proste nepujde, jak uz jsem pochopil. To bude urcite hrozny vykonostni propad, kdyz se takhle budou vsechny argumenty vkladat na zasobnik dvakrat jakoby dvakrat - jednou pri volani te public metody a pak pri volani te privatni.
-
V tom tvem pripade by to pak zvolilo spatny index do TVM no. Se prave snazim vymyslet, jak tohle resit jinak, nez podobnym zpusobem, jak si ukazoval. Ono to ale asi bez toho zdvojnasobeni poctu volani metod proste nepujde, jak uz jsem pochopil. To bude urcite hrozny vykonostni propad, kdyz se takhle budou vsechny argumenty vkladat na zasobnik dvakrat jakoby dvakrat - jednou pri volani te public metody a pak pri volani te privatni.
No, ještě by sis mohl udělat vlastní virtuální metody, ale to už by byl docela úlet :)
#include <iostream>
class A
{
public:
A();
private:
static void metoda1_impl(A* x);
static void metoda2_impl(A* x);
class A_table
{
public:
void (*metoda1_ptr)(A* x);
void (*metoda2_ptr)(A* x);
};
protected:
A_table *A_tbl;
public:
inline void metoda1()
{
A_tbl->metoda1_ptr(this);
}
inline void metoda2()
{
A_tbl->metoda2_ptr(this);
}
};
class B : public A
{
public:
B();
private:
static void metoda3_impl(B *);
static void metoda2_impl(B *); // nová implementace
class B_table
{
public:
void (*metoda3_ptr)(B *);
};
protected:
B_table *B_tbl;
public:
inline void metoda3()
{
B_tbl->metoda3_ptr(this);
}
};
A::A()
{
A_tbl = new A_table;
A_tbl->metoda1_ptr = &A::metoda1_impl;
A_tbl->metoda2_ptr = &A::metoda2_impl;
}
void A::metoda1_impl(A* x)
{
std::cout << "A::metoda1" << std::endl;
}
void A::metoda2_impl(A* x)
{
std::cout << "A::metoda2" << std::endl;
}
B::B()
{
B_tbl = new B_table;
A_tbl->metoda2_ptr = (void (*)(A *)) &B::metoda2_impl;
B_tbl->metoda3_ptr = &B::metoda3_impl;
}
void B::metoda3_impl(B* x)
{
std::cout << "B::metoda3" << std::endl;
}
void B::metoda2_impl(B* x)
{
std::cout << "B::metoda2" << std::endl;
}
int main()
{
B *b = new B;
b->metoda1();
b->metoda2();
b->metoda3();
A *a = new A;
a->metoda1();
a->metoda2();
a = b;
a->metoda1();
a->metoda2();
}
-
Taková poznámka. Mezi interfaci není uplně od věci provádět virtuální dědění. Je třeba si pouze uvědomit, že tím se přidává komplexita, takže si je třeba to rozmyslet. A pak že virtuální dědění je "nakažlivé". Jakmile to někde člověk použije, zpravidla pak musí všechno od toho místa dědit virtuálně, jinak se mu stane, že ho překladač častuje chybama nebo warningama o ambignuitě tříd a o jejich vzáhemném překrývání (expert je na tohle gcc při levelu -wextra)
Virtuální dědění ovšem dělá trošku jiný layout mezi potomkem a předkem.
Normální dědění jen rozšiřuje tabulku virtuálních metod na dalších indexech. Takže pokud má předek 5 metod a potomek 6, pak předek obsadí indexy 0 - 4 a potomek 5 - 10.
Virtuální dědění ale znamená, že předek bude mít svojí tabulky virtuálních adres. Potomek má pak v tabulce na nultém odkazu offset na předka a následují virtuální metody na indexech 1 - 6.
Když se z pointeru na potomka volá metoda předka, musí se nejprve vyzvednout offset na indexu 0, ten se přičte (odečte) od pointeru a tím se získá adresa tabulky předka a tam se metoda vyhledá. Je tam určitě dvojitá dereference, což může být další zdržení.
Výhodou virtuálního dědění interfaců je, že každý interface je v objektu jen jednou a k přetypování není nutné komplikovaným způsobem postupovat po hierachii jako u normálního dědění, pokud je treba přetypovat na předka, který se v interfacu nachází dvakrát, nebo i vícekrát (protože pak překladač hlásí, že neví, na kterého předka vlastně má ukázat).
Virtuální dědění interfaců je menší zádrhel, než virtuální dědění celých objektů, kde i přístup k member proměnným se děje přes offsety a to i v rámci implementace metod. Navíc do toho vstupují konstruktory, které u virtuálního dědění vyžadují jiný způsob kontrukce než je člověk zvyklej (interfacy by konstruktory mít neměly)