Alokátory a operator new v C++

4. 1. 2012 19:43 (aktualizováno) Ondřej Novák

Každý programátor v C++ ví, že pomocí operátoru new můžeme zkonstruovat objekt na globální haldě. Někdy je však potřeba provést alokaci nového objektu jinde, než na globální haldě. Slovíčko někdy zde ale není úplně na místě, v praxi jsem si ověřil, že to přináší výhody, takže spíše by se sem hodilo slovíčko „často“.

To, kde se nakonec vytvoří prostor pro nový objekt je určeno speciálními objekty, které nazývám alokátory (tedy je to něco podobného jako std::allocator, ale mnohem obecnější, abstraktnější rovině). V článku představím rozhraní takového alokátoru. Nejprve bych ale zopakoval (pro některé) co všechno lze s operátorem new dělat.

Tento operátor provádí víceméně dvě operace.

  1. Alokuje blok paměti tak velký, aby se do něj nově konstruovaný objekt vešel
  2. Provádí konstrukci objektu voláním konstruktoru.

Už pomocí standardních knihoven lze jednu nebo druhou funkci potlačit. Můžeme požadovat pouze konstrukci objektu v již alokovaném bloku a můžeme požadovat pouze alokaci bloku bez konstrukce objektu.

  1. operator new(velikost) – kde velikost je požadovaná velikost v bytech
  2. new(adresa) Foo – konstruuje objekt … instanci třídy Foo na zadané adrese.

Můj soukromý průzkum ukazuje, že první vlastnost operátoru new spoustu mých profesních kolegů nezná. Často by v této situaci použili malloc(). Upozorňuji, že to není totéž, přestože ve výchozím stavu toto volání nakonec ten malloc() zavolá.

Druhý bod představuje variantu použití new zvanou placement new. Zde se použije adresa již kdesi v paměti vyhrazeného bloku, kam bude objekt zkonstruován. Je to vlastně jediný legální způsob, jak nad nějakým kusem paměti zavolat konstruktor a tím z toho kusu paměti vytvořit něco smysluplného. K této variantě neexistuje operator delete, a zkonstruovaný objekt musí být zlikvidovat ručním zavoláním destruktoru.

Pro použití s alokátory využijeme placement new který obecně připouští použití libovolného množství dodatečných argumentů libovolného typu. Samozřejmě každá varianta musí mít vlastní prototyp. Operátor můžeme deklarovat jako globální nebo členskou funkci.

Operator new a třídy

Pracovat s globálním new je trochu obtíž, protože se může dostat do konfliktu. Vlastní verzi přetíženého new nelze umístit do namespace, takže bude vždy globálním a představovat riziko konfliktu s jinou definicí v jiné knihovně. Dá se to obejít, ale nechci se tím teď zabývat.

Mnohem raději používám přetížení operátoru new u tříd. Co to znamená? Pokud se někde konstruuje třída, která má deklarovaný členský operator new, pak se k alokaci volá právě tento operator namísto globálního. A pozor, platí to i pro všechny podtřídy(!) Žádné konflikty nehrozí a pro knihovní třídy mám možnost nabídnout rozšířené alokace bez velké námahy. Stačí, když všechny třídy podědí nějakou společnou, která standardní operátor new (a samozřejmě i patřičné delete) přetíží.

Takže malý příklad, nejprve si ale vytvoříme jednoduchý alokátor.

 class IRuntimeAlloc {

 public:

     virtual void *alloc(std::size_tobjSize)  = 0;

virtualvoiddealloc(void *ptr, std::size_tobjSize)  = 0;

 };

Nehledejte tam nic světoborného. Název IRuntimeAlloc je možná trochu nesmyslný, ale vznikl historicky s potřebou mít alokátor, který nezná velikost alokovaného objektu během překladu, ale až za běhu (runtime). Druhým důvodem je použití pozdní vazby, také v runtime, protože většina mých alokátorů jsou šablony bez pozdních vazeb. Zvolil jsem tenhle příklad proto, že je nejjednoduší. Funkce alloc a dealloc kopírují prototypy funkcí new a delete, takže asi nebude těžké volat je přímo z jejich implementace. Až tedy na jeden detail, a to přítomnost velikost objektu u dealloc. Mám vyzkoušeno, že je dobré, když tuto informaci alokátoru dodáme z vnějšku, protože alokátor tak ušetří spoustu místa ukládáním tohoto čísla. Věřte nebo ne, existuje způsob, jak tuto velikost získat bez nutnosti si jí ukládat poblíž alokovaného bloku. Dodá nám jí překladač.

classDynObject{public:

   void *operatornew(std::size_tsz);void *operatornew(std::size_tsz, IRuntimeAlloc &alloc)

   voidoperatordelete(void *ptr, std::size_tsz);};

Teď by tedy mělo postačit všech knihovní třídy nechat dědit třídu DynObject a mohu vesele používat jak standardni new Foo; tak new(alloc) Foo; kde alloc je reference na objekt implementující rozhraní IRuntimeAlloc.

Povšimněte si operátoru delete. Opět můj soukromý průzkum zjistil, že není mnoho kolegů, kteří by si vzpomněli, že existuje tato varianta. Většina si vzpomněla jen na delete(void *ptr). Výše uvedenou variantu lze použít pouze u tříd; nebude fungovat u globálního delete. Zato bude správně fungovat i u všech potomků. A to není vše, pokud tuto třídu podědíme v jiné vrstvě hierarchie, než základní třídou (base), bude operator správně volán i v případě, že objekt bude destruován přes ukazatel na základní třídu … tedy za předpokladu, že třída má virtuální destruktor!!!! (což je u drtivé většině tříd povinnost)

Výhodou varianty delete s uvedenou velikosti je, že si nemusíme velikost objektu nikde pamatovat. Aby to nebylo tak skvělé, máme tu i jednu nevýhodu. Všimněte si, že operátor delete neví nic o použitém alokátoru a přesto je nutné, aby deallokace proběhla přes alokátor, a nikoliv přes standardní delete. I když tedy ušetříme místo pro uložení velikosti objektu, musíme navíc přidat odkaz na alokátor, tak, aby delete vědělo, kdo je za deallokaci zodpovědný. V paměti se pak alokuje o něco větší blok (typicky o velikost ukazatele)

Prostor pro objekt allocptr

Pokud některého čtenáře napadlo, že by bylo možné použít něco jako „placement delete“ a allokátor mu dodat jako parametr, tak na to honem rychle zapomeňte. Nic takového nejde.

Výjimky … a bug v normě.

Nic člověka nepotrápí víc, než výjimky a obzvlášť u přetížených new a delete. Pokud během konstrukce objektu dojde k výjimce, je třeba přidělené místo opět uvolnit. A tady C++ volí jinou cestu, nedojde k volání standardního delete, ale k jakési obdobě „placement delete“. V normě C++ je uvedeno, že prototyp takové funkce je totožný s new až tedy na to, že jde o delete a prvním parametrem je ukazatel na alokovaný blok. Zde je několik příkladů:

Funkce pro alokaci Funkce pro dealokaci při výjimce
operator new(size_t sz) operator delete(void *ptr) nebo operator delete(void *ptr, std::size_t sz)
operator new(size_t sz, void *ptr2) operator delete(void *ptr, void *ptr2)
operator new(size_t sz, IRuntimeAllocator &a) operator delete(void *ptr, IRuntimeAllocator &a)

Pokud tam takové funkce zapomeneme uvést, dojde při výjimce k leaku (k neuvolnění alokované paměti). Což není zrovna chytré. Takže je tam přídáme.

classDynObject {

 public:

    void *operatornew(std::size_tsz);void *operatornew(std::size_tsz, IRuntimeAlloc &alloc); 


    voidoperatordelete(void *ptr, std::size_tsz);voidoperatordelete(void *ptr, IRuntimeAlloc &alloc); };

Vidíte tam nějakou zradu? Já ano, a dost zásadní. Jak jsem napsal výše, náš alokátor vyžaduje velikost alokovaného bloku, aby jej mohl dealokovat. Je fajn, že nám to překladač spočítá, pokud jde o standardní delete, ale v případě dealokace během výjimky nemáme šanci velikost zjistit !!! To je dost zásadní problém. Z tohoto pohledu to vypadá, že jde o pořádný průšvih v normě C++, velikost uvedená u delete je vlastně  k ničemu, protože si ji beztak musím uložit někde poblíž alokovaného bloku. Hledal jsem tedy způsob, jak to obejit a nakonec se mi to podařilo obejít využím pomocného objektu, který bude existovat pouze během alokace paměti a konstrukce nového objektu. Tento objekt vznikne na zásobníku a aby jej programátor nemusel znát, využijeme implicitní volání konstruktoru. Nějak takto:

classDynObjectAllocHelper {public:

    DynObjectAllocHelper (IRuntimeAlloc &alloc):alloc(alloc),sz(0) {}

    IRuntimeAlloc & ref() const {returnalloc;}

    voidstoreSize(std::size_tsz) const {this->sz = sz;}

    std::size_tgetSize() const {returnsz;}

 protected:

    IRuntimeAlloc & alloc;

    mutablestd::size_tsz;

 };

Prototyp třídy DynObject náležitě upravíme:

classDynObject {

 public:

    void *operatornew(std::size_tsz);void *operatornew(std::size_tsz,const DynObjectAllocHelper &alloc);

    voidoperatordelete(void *ptr, std::size_tsz);

    voidoperatordelete(void *ptr);voidoperatordelete(void *ptr,const DynObjectAllocHelper &alloc); 
};

Použití const reference způsobí, že instance třídy DynObjectAllocHelper se bude konstruovat v zásobníku volajícího a teprve pak se použije pro volání new případně delete při výjimce. Protože konstruktor třídy není deklarován jako explicit a má jeden parametr, není jej třeba uvádět, stačí do new dodat referenci na allokátor a překladač sám zajistí implicitní konverzi a konstrukci pomocného objektu. Metoda storeSize ukládá do pomocného objektu velikost předanou funkcí new tak, aby si jí funkce delete mohla vyzvednou, pokud dojde k výjimce (má k dispozici referenci na objekt DynObjectAllocHelper předanou parametrem). Protože je objekt s kvalifikátorem const, musíme proměnnou pro velikost deklarovat jako mutable. Čistotu kódu v tom nehledejte, jedná se vlastně o hack. Pomocný objekt je destruován automaticky, jakmile je konstrukce alokovaného objektu dokončena, nebo pokud je dokončen úklid při výjimce.

Využití

Jak už bylo na začátku naznačeno, smyslem bylo zavést možnost alokovat objekty pomocí jiných alokátorů. Uvedu pár alokátorů z mé knihovny pro inspiraci

AllocInBuffer – alokátor, kterému předávám adresu a velikost paměti, ve které si může alokovat. Jedná se o nejjednodušší alokátor, který lze použít jen na alokaci jednoho objektu. Je to obdoba placement new s tím rozdílem, že podporuje i delete, takže na alokovaný objekt lze použít chytrý ukazatel.

char buffer[256];
AllocInBuffer bufalloc(buffer,sizeof(buffer));
MyObject *x = new(bufalloc) MyObject;
//...

delete x;

TempAlloc – alokátor optimalizovaný pro krátkodobé alokace, které uspokojuje se složitosti O(1). Nevýhodou je, že nevhodné použití vede na výrazné plýtvání paměti.

Varianta, kdy o umístění rozhoduje volaný

MyObject *foo() {
   //funkce foo() předává výsledek výpočtu a
   //k alokaci použije alokátor pro krátkodobé alokace

   return new(TempAlloc::getInstance()) MyObject;
}
void bar() {
   MyObject *x = foo();
   //... zpracuji x
   // funkce bar() zpracuje výsledek a paměť uvolní
   // nemusí tušit, že objekt je alokován v dočasné paměti

   delete x;
}

Varianta, kdy o umístění rozhoduje volající

MyObject *foo(IRuntimeAlloc &alloc) {
   //funkce foo() předává výsledek výpočtu a
   //k alokaci použije dodaný alokátor

   return new(alloc) MyObject;
}
void bar() {
   //volající ví, že vytvořený objekt bude mít velmi krátkou životnost
   MyObject *x = foo(TempAlloc::getInstance());
   //... zpracuji x

   delete x;
}

ClusterAlloc – alokátor objektů stejné velikosti, snižuje fragmentaci paměti a dosahuje konstantní složitosti a dobré lokality dat (ukládají se do clusterů)

FastAlloc – dealokované bloky udržuje ve spojovém seznamu a při alokaci dokáže velice rychle vydat takový volný blok bez nutnosti volat globální alokátor. Je dobré, pokud každé vlákno má vlastní instanci, zvyšuje to propustnost alokací.

V článku nehledejte implementaci třídy DynObject, k té bych se dostal příště, bude tam pár dobrých nápadů které mohou ve výsledku urychlit alokace a dealokace pomocí allokátorů.

Sdílet

  • 6. 1. 2012 9:52

    Marek (neregistrovaný)

    Pěkný článek, mám pár dotazů: jak máte implementováno TempAlloc::ge­tInstance() - předpokládám, že jde o singleton. S přihlédnutím k tomu že "double checked locking" v C++ nefunguje (http://www.aristeia.com/Papers/DDJ_Jul_Aug_2004_revised.pdf a podobné). Druhý dotaz: jak na alocátory a STL? Tam není podle mě dost dobře možné využít třeba Váš BufferedAlloc, protože kontejnery STL přijímají typ nikoli instanci a nejsou nuceny si držet instanci alokátoru a tu si při kopírování předávat. Offtopic: Hele, tady nefunguje MojeID.
    Marek.

  • 26. 8. 2024 11:05

    Tomas Mark

    Ahoj, snažil jsem se prokousat přes tento článek a nakonec takto vypadá code, který můžete copy paste vyzkoušet.

    #include <cstddef>
    #include <iostream>
    
    // Rozhraní alokátoru
    class IRuntimeAlloc
    {
    public:
        virtual void *alloc(std::size_t objSize) = 0;
        virtual void dealloc(void *ptr, std::size_t objSize) = 0;
    };
    
    class DynObjectOld
    {
    public:
        // Standardní operátor new
        void *operator new(std::size_t sz)
        {
            std::cout << "Standard new\n";
            return ::operator new(sz);
        }
    
        // Přetížený operátor new s alokátorem
        void *operator new(std::size_t sz, IRuntimeAlloc &alloc)
        {
            std::cout << "Custom new with allocator\n";
            return alloc.alloc(sz);
        }
    
        // Standardní operátor delete
        void operator delete(void *ptr, std::size_t sz)
        {
            std::cout << "Standard delete\n";
            ::operator delete(ptr);
        }
    
        // Přetížený operátor delete s alokátorem
        void operator delete(void *ptr, IRuntimeAlloc &alloc)
        {
            std::cout << "Custom delete with allocator\n";
            alloc.dealloc(ptr, sizeof(DynObjectOld));
        }
    };
    
    class DynObjectAllocHelper
    {
    public:
        DynObjectAllocHelper(IRuntimeAlloc &alloc) : alloc(alloc), sz(0) {}
    
        IRuntimeAlloc &ref() const { return alloc; }
        void storeSize(std::size_t sz) const { this->sz = sz; }
        std::size_t getSize() const { return sz; }
    
    protected:
        IRuntimeAlloc &alloc;
        mutable std::size_t sz;
    };
    
    class DynObject
    {
    public:
        // Standardní operátor new
        void *operator new(std::size_t sz)
        {
            std::cout << "Standard new\n";
            return ::operator new(sz);
        }
    
        // Přetížený operátor new s alokátorem
        void *operator new(std::size_t sz, IRuntimeAlloc &alloc)
        {
            std::cout << "Custom new with allocator\n";
            return alloc.alloc(sz);
        }
    
        // Přetížený operátor new s pomocníkem
        void *operator new(std::size_t sz, const DynObjectAllocHelper &alloc)
        {
            std::cout << "Custom new with helper\n";
            void *ptr = alloc.ref().alloc(sz);
            alloc.storeSize(sz);
            return ptr;
        }
    
        // Standardní operátor delete
        void operator delete(void *ptr, std::size_t sz)
        {
            std::cout << "Standard delete\n";
            ::operator delete(ptr);
        }
    
        // Přetížený operátor delete s alokátorem
        void operator delete(void *ptr, IRuntimeAlloc &alloc)
        {
            std::cout << "Custom delete with allocator\n";
            alloc.dealloc(ptr, sizeof(DynObjectOld));
        }
    
        // Přetížený operátor delete s pomocníkem
        void operator delete(void *ptr, const DynObjectAllocHelper &alloc)
        {
            std::cout << "Custom delete with helper\n";
            alloc.ref().dealloc(ptr, alloc.getSize());
        }
    };
    
    class AllocInBuffer : public IRuntimeAlloc
    {
    public:
        AllocInBuffer(void *buffer, std::size_t size) : buffer(static_cast<char *>(buffer)), size(size), used(0) {}
    
        void *alloc(std::size_t objSize) override
        {
            if (used + objSize > size)
                return nullptr;
            void *ptr = buffer + used;
            used += objSize;
            return ptr;
        }
    
        void dealloc(void *ptr, std::size_t objSize) override
        {
            // Tento jednoduchý alokátor neumožňuje dealokaci jednotlivých objektů
            // Nicméně by mohl resetovat celý buffer
            std::cout << "Buffer deallocated\n";
        }
    
    private:
        char *buffer;
        std::size_t size;
        std::size_t used;
    };
    
    
    int main() {
        char buffer[256];
        AllocInBuffer bufalloc(buffer, sizeof(buffer));
    
        // Alokace objektu pomocí vlastního alokátoru
        DynObject* obj = new(bufalloc) DynObject;
    
        // Uvolnění objektu
        // delete obj; // vyvola vyjimku
        obj->DynObject::operator delete(obj, bufalloc);
    
        return 0;
    } 
  • 6. 1. 2012 14:14

    Ondřej Novák (neregistrovaný)

    @1 Zdravím. Ve skutečnosti nemám nikde singletony, tedy pouze jen objekty, které mohou býti singletony, nebo jsou na to vhodné. To getInstance() je funkce, která přes mnou napsaný systém alokace singletonů prostě vytvoří a následně zaregistruje globální instanci objektu, která je pak dostupná přes stejné volání (samozřejmě je to MT safe). Nicméně instancí téže třídy lze vytvořit více, není tam to omezení jako u pravých singletonů.

    Alokátory v STL jsou fakt zmršené, právě že si nedrží instanci, musí to člověk obcházet třeba přes globální proměnné, nebo lépe přes thread proměnné. Já mám ještě v plánu kromě druhého dílu k tomuto článku pak povídání o továrnách (obecných), které se hodí třeba na alokaci uzlů ve stromě, nebo ve spojových seznamech, a dále tam mám třídu představující alokovaný blok, což se zase hodi pro všelijaké vektory, a obecně kontejnery pracující se souvislým úsekem paměti s možností jeho relokace.

    Proto jsem se dostal tak daleko, že ve svých knihovnách mám i vlastní implementace vektorů, vyhledávacích stromů, listů, front, zásobníků, a tak dále, jenom proto, že v nich mohu používat své alokátory :-)

  • 7. 1. 2012 19:59

    Jenda (neregistrovaný)

    Zajimavy zapisek. @2, ten "vami napsany system alokace singletonu" teda vytvari neco jako service oriented architecture (inversion of control)?

  • 9. 1. 2012 12:22

    Ondřej Novák (neregistrovaný)

    @3 nevím jak to bylo myšleno, ale asi jsem to špatně popsal. Spíš jde o to, že řeším po svém problém, kdy inicializace statických proměnných uvnitř funkce ... kdy se inicializuje prvním průchodem ... není MT Safe. Postupně z toho vznikla šablona, která dostane parametrem typ, ze kterého má udělat singleton. A tato šablona implementuje metodu getInstance(), která vrací instanci parametru, jenž se inicializuje při prvním zavolání a při každém další už pouze vrací její referenci. A to všechno MT Safe za pomocí spinlocku.

  • 14. 5. 2012 15:33

    Sten (neregistrovaný)

    [2] Alokace v STL jsem obcházel přes kopírovací konstruktory (obsahující pouze base adresu daného alokátoru) a globální mapu alokátorů, kterou používalo i delete. Kdokoliv mohl dohledat, která část paměti patří kterému alokátoru.

  • 5. 9. 2012 12:20

    PavDub (neregistrovaný)

    Velmi zajímavý nápad.

    Napadá mě nicméně:
    1) Vzhledem k tomu, že je stejně třeba volat pro konstrukci objektu zvláštní verzi operator new(size_t sz, IRuntimeAllocator &a), pak ůže stejně dobře posloužit např. template T * Create(IRunti­meAllocator &a). Pokud bude fce Create napsaná s oddělením alokace a konstrukce objektu T (viz STL), tak není třeba aby všechny třídy poskytovali rozhraní class DynObject, takže celý přístup bude rovněž (zpětně) kompatibilní s externími typy.

    2) Co se týká alokátorů v STL tak na nich opravdu nic "zmršeného" nevidím. Pokud už je (výjimečně) opravdu potřeba, aby byl objekt (de)alokován určitou konkrétní instancí alokátoru, pak může být tato (resp. reference na ni) uložena unvnitř Objektu - čímž se vracíme tak trochu zpět k rozhraní DynObject a přicházíme tak o zpětnou / externí kompatibilitu - nebo v mapě Objekt_pointer-Allokator_reference - čímž vzniká určitá režie při (de)alokaci.

    p.s.: co se týče tématu "Alokátory a operator new v C++" tak bych osobně spíš normě vytknul, že šla "rozštěpenou" cestou obou přístupů zároveň a ještě k tomu umožnila přetěžovat new jako členskou třídu. Vzniká tak trochu bordel a v podstatě člověk nikdy neví, co externí objekt vlastně dělá při svojí dyn. alokaci. Osobně bych dal naopak přednost pouze objektu Allokator a operatory new a delete vůbec nezaváděl :)

  • 5. 9. 2012 14:21

    Ondřej Novák (neregistrovaný)

    [6] k tomu asi pár námitek.

    1) Vůbec to není o továrnách (což taková funkce Create mimochodem je). V C++ existuje jediná legální cesta, jak vytvořit objekt a to pomocí operátoru new. Nic jiného není možné, nebo jde o hack. I kdyby se to řešilo extra alokací a pak konstrukcí, i placement new je operator new. A samozřejmě výše uvedený systém nebrání takovou funkci napsat. Nicméně pořád je pro mě jednodušší napsat šablonu, která podědí T a DynObject a výslednou instanci (šablony) nechat zkonstruovat přes new(...).

    K tomuto bodu ještě jedna námitka. Problém šablony je ten, že předpokládáte konstrukci objektu bez parametů. Ale co když konstruktor vyžaduje nějaké parametry? Já mám dost objektů, které se nedají zkonstruovat bez parametrů. Jsou to zpravidla "satelity", pomocné objekty navázané na nějaký hlavní objekt a jeho referenci dostávají právě parametrem v konstruktoru. A zpravidla jsou ty satelity alokované nějakým speciálním alokátorem

    2) STL alokátory nelze instanciovat. Tečka. Tím jsou pro mě nepoužitelné (nemohu vytvořit instanci alokátoru, který by třeba spravoval nějaký kus paměti, kde by prováděl alokace).

    K druhé části bodu 2, opět je to totéž. Dealokaci v C++ smí provádět pouze a jenom pouze operátor delete. Pokud dealokujete zavoláním destruktoru a pak uvolnění paměti, tak potěš koště! Jak zjistíte velikost objektu? Co když má virtuální destruktor? Správnou velikost objektu poskytuje pouze operátor delete a to pouze ten přetížený ve třídě. A důvod, proč je vhodné znát velikost objektu při dealokaci jsem myslím uváděl.

    Dávat referenci dovnitř objektu je intruzivní řešení, které navíc trpí problémem rozštěpené zodpovědnosti. Jak se dostane tato reference do objektu? Kdo jí tam umístí? Alokátor nemůže, protože v době, kdy alokuje ten objekt ještě neexistuje. A konstruktor taky nemůže, protože ten zase nemusí o nějakém speciálním alokátoru vědět. Leda že by ho dostal jako parametr, ale pak jej zase uvádíme dvakrat (jednou jej voláme a pak předáváme konstruktoru, zajistěte, že vždycky půjde o tentýž objekt. Opět budete potřebovat nějakou továrnu). Tohle všechno věc šíleně zešložiťuje. A teď si k tomu přidejte takový exception handling kolem toho.

    p.s. A znova. K vytvoření objektu se používá new a k destrukci objektu se používá delete. Cokoliv jiného vede na zprasený neportabilní kód. Dodržujme prosím všechna doporučení tohoto nádherného jazyka. Nepleťte do toho chytré ukazatele, ty mají fungovat nad tímto systémem. A zase si hodně zjednoduším práci, když budu vědět, že chytrý ukazatel může objekt jednoduše dealokovat přes delete, aniž by musel nějaký alokátor znát.

    Moje PS (třída std::allocator slouží spíš k alokaci prvků ve vektoru. To je trochu jiná úloha, než alokace dynamických objektů)

  • 6. 9. 2012 8:43

    PavDub (neregistrovaný)

    Dále už by to asi bylo na delší diskuzi nad konkrétními potřebami a detaily implementace... Tak jen ještě drobné reakce k několika bodům výše (omlouvám se za délku příspěvku(ů))

    * To, že se jedná o zajímavý nápad jsem myslel velmi vážně a ještě dodávám, že je určitě i užitečný.
    * Fce Create je v tomhle případě asi taková továrna, jako operátor new itself ;)
    - - pardón, já musel... :)
    * Problém implicitního konstruktoru lze většinou řešit kopírovacím konstruktorem (allocator::con­struct)
    - - nemusí být Váš případ, nebo nemusí být praktické pro Váš případ
    * STL alokátory lze instanciovat a taky se to dělá, jelikož žádná z jejich metod neni static. Neřkuli vlastní alokátory.
    * Ano, s velikostí bloku při delete je problém. Viz můj povzdech v ps. a Váš povzdech k void operator delete(void *ptr,const DynObjectAlloc­Helper &alloc);
    * Virtuální destruktory viz např. http://www.cplusplus.com/reference/std/memory/allocator/destroy/
    * S referencí dovnitř objektu jsem se vyjádřil trochu zbrkle. V podstatě jsem měl na mysli Váš allocptr
    * používáni alokátorů rozhodně nevede na zprasený neportabilní kód - co celá STL ? (btw std::allokator stejně nakonec volá new a delete)


    shrnutí:
    Asi jsem se nechal trochu strhnout názvem článku "Alokátory a operator new v C++". Co se týče operátoru new a jeho přetěžování je tenhle článek supr. O Alokátorech toho ale moc neříká. Stále mi tedy připadá nešťastné, že norma zavedla jak operatory new a delete, tak koncept alokátorů (v STL). Osobně bych dal přednost alokátorům, protože zcela abstrahují paměťový model, což:
    - právě řeší problém rozštěpené odpovědnosti, tj. přenáší práci s paměťovým modelem výhradně na alokátor a alokované objekty se o to nestarají
    - což také zaručuje zpětnou/externí kompatibilitu, což mimo jiné např. zaručuje, že si člověk nemusí psát hned celou podmnožinu STL kontejnerů, kvůli novým paměťovým modelům ;)
    - lze alokovat paměť mimo RAM - což je také, proč jsem se o to kdysi zajímal

  • 6. 9. 2012 9:08

    Ondřej Novák (neregistrovaný)

    Dobře, tak to vypadá, že vidíme stejné problémy, ale hledáme jiná řešení. Já si s tím hraju už poměrně dlouhou a tohle je zatím systém, který se mi nejvíc osvědčil. Prošel jsem si cestou, kterou máte shrnuté v bodech přesně v tom pořadí.
    * Používal jsem továrny a kopírovací konstruktory.
    * Řešil instanciovatelnost stl alokátorů (a našel takové podivnosti, jako allocator::rebind, které hojně využívají STL kontejnery - takové nečekané překabátění allokátorů, které se jinak má řešit pomocí dvojice tříd = hlavní instance alokátoru, který umí vytvořit specifický alokátor ke konkrétnímu T)
    * Pak zápasení s velikostí objektu. Destrukce objektu "in-place" znám už dlouho a funguje to i na virtuální destruktor, ale jak zjistit velikost toho objektu, to nejde
    * Jak zjistím začátek objektu? dynamic_cast<void *> (co když máte vypnuté RTTI? co když tomu předložíte objekt, který nemá z principu RTTI záznam, třeba třídu s nevirtuálními funkcemi). Jo, vím, že boostu na to mají nějakou "elektrárnu"

    Ohledně z praseného kódu, trochu si rejpnu do STL, jako žo ho celý považuju za zprasený (podívejte se někdy dovnitř. Proč třeba std::string řeší aktuální jazykovou stránku operačního systému? - MSVC). Takže STL nevyužívám, pakliže mi to někdo nedá příkazem. Určitě i new a delete by se daly vymyslet lépe, ale tam já už jako uživatel jazyka moc nezmůžu. Dokud překladač nebude zvládat genericky varargs a s nimi související perfect forwarding (to sice C++11 umí, ale je to opět velice "nehezké" a nezvládá ty varargs), tak si s new prostě musíme vystačit. Ony ty kopírovací konstruktory v továrnách jsou dost rušivé. Některé objekty zpravidla nemají kopírovací konstruktory, protože to postrádá smysl. Zkuste třeba zkopírovat síťové spojení. Takže i já občas do dokumentace píšu, že kopírovací konstruktor kopíruje pouze "setup" objektu, nikoliv jeho aktuální stav.

    K tomu závěru. Lidé zblblí STLkem - oficiálně standardem, ale u mě spíš "doporučením" (a ještě k tomu mizerným) při použití slova "alokátor" okamžitě vidí std::allocator a přes to vlak nejede. Já mám ale opravdu na mysli obecné alokátory, tedy objekty, které zajišťují alokaci paměti. Takže název článku je správně. Je to i o alokátorech (ne o std::allocatorech)

    PS:

    template class TDynObject: public T, public DynObject {...};
    TDynObject<std::vec­tor<int> > *k = new(myalloc) TDynObject<std::vec­tor<int> >;

    Jo, forwarding parametrů konstruktoru u TDynObject je opět obligátní problém. Ale dá se to pořešit v rozumé míře (do 8 argumentú)

  • 6. 9. 2012 9:55

    PavDub (neregistrovaný)

    - Nojo rebind - tam teprve začíná lahůdkářství :D
    - Jo, jsem tak trochu fanda STL : pravda, že spíš té myšlenky, než některých jejích implementací...
    - Nejsem zas ale uplně zblblej STLkem, nicméně "c++" a "alokátor" celkem asociuje std::Allocator ... po krátkém zamyšlení ale uznávám, že lepší název článku by člověk vymyslel těžko
    - jo, to ps. mě samozřejmě nenapadlo...

    Hezká diskuze s rozumným partnerem... :)
    EOF