std::vector von MSVC



  • Hallo,

    ich nutze Visual Studio 2013 EE und konnte ein merkwürdiges Verhalten feststellen. Ich habe einen Allocator-Wrapper geschrieben, der die durchgeführten Aktionen festhält und an einen darunterliegenden Allocator weiterleitet. Folgender Code:

    {
    		typedef al::LoggingAllocator<int, std::allocator> AllocT;
    		std::vector<int, AllocT> Foo(AllocT(&std::cout));
    		std::cout << Foo.size() << ':' << Foo.capacity() << '\n';
    	}
    

    Die Ausgabe ist die folgende (* bedeutet, dass die Aktion den Allocator betrifft, sprich, dass der Allocator konstruiert wurde und nicht ein Objekt):

    LoggingAllocator<int> Id 0: * Constructed from LoggingDestination (0F1AC1B0)
    LoggingAllocator<int> Id 1: * Copy-constructed (0)
    LoggingAllocator<int> Id 2: * Copy-constructed (1)
    LoggingAllocator<struct std::_Container_proxy> Id 3: * Constructed from another LoggingAllocator<> (2)
    LoggingAllocator<struct std::_Container_proxy> Id 3: Allocating a block of 1 element(s)...
    LoggingAllocator<struct std::_Container_proxy> Id 3:    success: 0038BA88
    LoggingAllocator<struct std::_Container_proxy> Id 3: Copy-constructing an object (0038BA88)...
    LoggingAllocator<struct std::_Container_proxy> Id 3:    success
    LoggingAllocator<struct std::_Container_proxy> Id 3: * Destructed
    LoggingAllocator<int> Id 1: * Destructed
    LoggingAllocator<int> Id 0: * Destructed
    0:0
    LoggingAllocator<struct std::_Container_proxy> Id 4: * Constructed from another LoggingAllocator<> (2)
    LoggingAllocator<struct std::_Container_proxy> Id 4: Destroying an object (0038BA88)...
    LoggingAllocator<struct std::_Container_proxy> Id 4:    success
    LoggingAllocator<struct std::_Container_proxy> Id 4: Deallocating a block (0038BA88) of 1 element(s)...
    LoggingAllocator<struct std::_Container_proxy> Id 4:    success
    LoggingAllocator<struct std::_Container_proxy> Id 4: * Destructed
    LoggingAllocator<int> Id 2: * Destructed
    

    Dabei ist die Ausgabe beim Debug-Build gleich wie beim Release-Build.

    Meine Fragen:
    1. Darf der Container Objekte mit einem anderen Allocator löschen / zerstören als mit demjenigen, mit welchem sie alloziert / konstruiert wurden?
    2. Darf der Container sein internes Zeug ( std::_Container_proxy ) mit seinem Allocator anfordern?
    3. Wozu braucht die MSVC-Implementierung dynamischen Speicher für einen leeren Container? Und wieso das selbst im Release-Build? Ich wurde aus dem Code nicht so richtig schlau auf die Schnelle, vielleicht weiss da jemand mehr dazu.

    Gruss

    Edit: Aufgrund meines menschlichen Versagens war das, was ich über die Release-Builds gesagt habe, nicht ganz korrekt.
    Ausgabe vom Release-Build:

    LoggingAllocator<int> Id 0: * Constructed from LoggingDestination (6B856198)
    LoggingAllocator<int> Id 1: * Copy-constructed (0)
    LoggingAllocator<int> Id 0: * Destructed
    0:0
    LoggingAllocator<int> Id 1: * Destructed
    

    Edit 2:
    Bei Fundamentals ruft std::vector AllocatorT::destroy gar nicht auf vor AllocatorT::deallocate (resp. das Äquivalent aus std::allocator_traits ). Darf er das?



  • asfdlol schrieb:

    1. Darf der Container Objekte mit einem anderen Allocator löschen / zerstören als mit demjenigen, mit welchem sie alloziert / konstruiert wurden?

    Soweit ich weiss war das immer erlaubt, sofern der andere Allocator als Kopie des ursprünglichen Allocators erstellt wurde.

    asfdlol schrieb:

    2. Darf der Container sein internes Zeug ( std::_Container_proxy ) mit seinem Allocator anfordern?

    Ich schätze ja. Bin aber kein Experte auf dem Gebite. Was sicher OK ist, ist über ::rebind andere Allocator-Typen zu erzeugen, und damit dann z.B. einen Allocator für Tree-Nodes zu erzeugen (die dann ja üblicherweise das User-Item mit enthalten).

    asfdlol schrieb:

    3. Wozu braucht die MSVC-Implementierung dynamischen Speicher für einen leeren Container? Und wieso das selbst im Release-Build? Ich wurde aus dem Code nicht so richtig schlau auf die Schnelle, vielleicht weiss da jemand mehr dazu.

    Muss ich mir angucken. Kann mir aber nicht vorstellen dass ein leerer MSVC vector dynamisch Speicher anfordert. Das wäre ja ganz krass böse.



  • OK. Also.

    Ohne Iterator-Debugging macht MSVC's vector genau 0 Allokationen so lange man nix rein tut (=Default im Release Build). So wie es sein soll.
    Mit Iterator-Debugging erzeugt er immer dynamisch einen _Container_proxy .

    Dieser ist für's Iterator-Debugging nötig und speichert den Zeiger auf den ersten Iterator des Containers.
    Damit swap O(1) bleiben kann muss der _Container_proxy dynamisch angefordert werden. (Die Iteratoren haben ihrerseits ja einen Zeiger auf den _Container_proxy , und wenn man den _Container_proxy nicht mittauschen könnte, dann müssten diese Zeiger in ALLEN Iteratoren umgeschrieben werden.)

    Was der Grund ist dass trotz leerem vector der _Container_proxy sofort erzeugt wird, kann ich nicht sagen, so genau hab ich mir den Code auch nicht angesehen.
    Könnte sein Faulheit, könnte aber genau so gut sein dass das Erlauben eines nicht-vorhandenen _Container_proxy an vielen Stellen Checks/Sonderbehandlungen erfordern würde, so dass es im Endeffekt bei durchschnittlichen Programmen sogar schneller den Proxy einfach immer zu erzeugen.

    asfdlol schrieb:

    Bei Fundamentals ruft std::vector AllocatorT::destroy gar nicht auf vor AllocatorT::deallocate (resp. das Äquivalent aus std::allocator_traits ). Darf er das?

    Wieso nicht?

    Du darfst Objekte in C++ ohne weiteres dadurch zerstören dass du einfach ihren Speicher freigibst.
    Bedingung dafür ist bloss dass das korrekte Funktionieren des Programms nicht davon abhängt dass der Dtor ausgeführt wird. Oder anders gesagt: der Dtor ist, abgesehen von den vielen Fällen wo C++ ihn automatisch für dich aufruft, eine ganz normale Funktion.
    "Magic" wie die des Ctor, der aus einem Speicherbereich, voll mit lauter Schrott aus alten Flippern, ein Objekt einer Klasse T erzeugen kann, das dann danach auch als T angesprochen werden darf, gibt es beim Dtor keine. *

    D.h. wenn du die Klasse deren Objekt du durch "einfach freigeben" zerstören willst kennst, und weisst dass die im Dtor nix tut, dann geht das OK.

    Zumindest ist das bei normalen, über new/placement new erzeugten Objekten so. Wenn es da für über Allokatoren angeforderte Objekte keine Spezialregel gibt, dann würde ich die "üblichen" sonst geltenden Regeln auch da anwenden, und das würde heissen: ja, ist OK.

    *: Du darfst in C++ ein Objekt auch einfach dadruch zerstören dass du es mit 'was anderem überschreibst. Natürlich wieder nur unter der Voraussetzung dass die Ausführung des Dtors wirklich unnötig ist.



  • Danke für die Antwort, sehr aufschlussreich. 🙂

    hustbaer schrieb:

    asfdlol schrieb:

    2. Darf der Container sein internes Zeug ( std::_Container_proxy ) mit seinem Allocator anfordern?

    Ich schätze ja. Bin aber kein Experte auf dem Gebite. Was sicher OK ist, ist über ::rebind andere Allocator-Typen zu erzeugen, und damit dann z.B. einen Allocator für Tree-Nodes zu erzeugen (die dann ja üblicherweise das User-Item mit enthalten).

    Ach ja, selbstverständlich... wie konnte ich das bloss vergessen...

    hustbaer schrieb:

    asfdlol schrieb:

    Bei Fundamentals ruft std::vector AllocatorT::destroy gar nicht auf vor AllocatorT::deallocate (resp. das Äquivalent aus std::allocator_traits ). Darf er das?

    Wieso nicht?

    Irgendwie hatte ich das Gefühl, dass auch zu jedem Ctor-Aufruf auch ein Dtor-Aufruf gehört, oder zumindest zu jedem construct ein destroy . Meine Intuition behielt wohl nicht Recht.

    Nach meinem Verständnis nachdem ich gelesen habe, was du geschrieben hast, könnte man mit std::is_trivially_destructible testen, ob ein destroy -Aufruf vonnöten ist, ja?



  • Ja, mit std::is_trivially_destructible müsste es wohl gehen.
    Wobei das vermutlich auch nur bei so Sachen wie Pool-Allokatoren was bringen würde, wenn man den ganzen Pool wegwerfen kann ohne die Dtors vorher aufzurufen. Wenn überhaupt.
    Der Compiler weiss ja dann auch dass std::is_trivially_destructible == true , und dann erzeugt er einfach keinen code für das p->~T() ;

    D.h. es ist vermutlich nicht super sinnvoll über solche Optimierungen wirklich nachzudenken. Aber in C++ ist ja einiges "legal" was kaum jemals bis gar nicht sinnvoll ist.


Anmelden zum Antworten