Virtual dtor and placement new.

Discussion in 'C++' started by Giancarlo Niccolai, Aug 16, 2004.

  1. Hello all.

    I have peeked through the FAQ and all relevant links, and also through
    Stroustrup book, but I have not been able to find an answer, so I have to
    post here as a last resort.

    It makes sense that if you have virtual destructors, they are eventually
    used in the explicit destructor call when using the placement new semantic:

    class A {
    ....
    virtual ~A() { cout <<"one" <<endl; }
    };

    class B: public A {
    ....
    virtual ~B() { cout << "two" << endl; }
    };

    ....

    A *mem = static_cast<A*>(new( good_memory ) B);

    // Now we have a B item in a placed good memory, which is pointed
    // by a base class A*

    Now, using the semantic

    mem->~A();
    free_good_memory( mem );

    won't do, as it won't scan the B object Vtable and will just feed the mem
    object in the A class destructor.

    The question is: how to use virtual destructors when dealing with placed
    memory?

    TIA,

    Giancarlo Niccolai
     
    Giancarlo Niccolai, Aug 16, 2004
    #1
    1. Advertisements

  2. There is no need for the cast. Conversion from a pointer to derived to
    a pointer to base is implicit and provided by the language.
    I don't think there is an answer. There is, however, a proposal to
    introduce "placement delete", probably specifically for that purpose.

    Victor
     
    Victor Bazarov, Aug 16, 2004
    #2
    1. Advertisements

  3. * Giancarlo Niccolai:
    Short answer is: don't use placement new.

    Especially, in light of the use of casting in your code,

    A *mem = static_cast<A*>(new( good_memory ) B);

    which is 100% unnecessary and misleading, absolutely don't use
    placement new.

    But to answer the question of "how", technically one approach is

    #include <iostream> // std::cout

    static char rawMemory[0x400];

    class A
    {
    public:
    virtual ~A() { std::cout << "~A()\n"; }
    static void* operator new( size_t size )
    {
    std::cout << "Allocating " << (unsigned long) size << "
    bytes.\n";
    return rawMemory; // Assume suitable alignment.
    }
    static void operator delete( void* ) {}
    };

    class B: public A
    {
    double d;
    public:
    virtual ~B() { std::cout << "~B()\n"; }
    };

    int main()
    {
    A* p = new B;
    delete p;
    }

    If you do provide an operator new( size_t ) for a class you should
    probably also provide an operator new[] as well as the identity
    form of placement new, but as already mentioned twice: don't.
     
    Alf P. Steinbach, Aug 16, 2004
    #3
  4. Short reply is: I have to. And I know what I am doing and why.
    Yes, It is 100% unnecessary. It was there to pose the emphasis on the fact
    that B and A are one the base class of the other in a visual way; and
    anyhow there is no need to correct a working thing, and to suppose that as
    I have not been minimal, but explicit, I am not knowing what I am doing.
    Please, don't teach me basics, I know them.

    What I didn't know, and I thank you for, was:
    Actually, it also works with overloaded:

    class myFunAllocator {
    ....
    };

    // overload new with myFunAllocator;

    class A {
    //... no new.... operator
    void operator delete( void * ) {
    ... do something that works with myFunAllocator;
    }
    };

    A *a = new( myFunAllocator ) B;

    delete a; // actually calls B destructor AND cleanly free memory with
    // myFunAllocator.

    This works also if using a placement new with placed new instead of an
    overloaded new.

    What I can't just take off of my head is: if this works (and it should,
    given C++ rules), why then is said (in FAQ, in BS book, other books and
    wherever I look) that placement new MUST be "closed" with explicit
    destructor call? Is there something in the definition of the delete
    operator that may fail if using it this way (i.e. overloading it and
    freeing the mem yourself).

    Thanks again,
    Giancarlo Niccolai.
     
    Giancarlo Niccolai, Aug 16, 2004
    #4
  5. I think it comes from the notion that creation and destruction of any
    object are the two operations that define its lifetime. You cannot
    simply free the memory the object allocates, you have to tell the system
    (and the object itself) that the object goes out of existence by calling
    (or causing the invocation of) the object's d-tor.

    Victor
     
    Victor Bazarov, Aug 16, 2004
    #5
  6. * Giancarlo Niccolai:
    Sorry about that; it's very difficult to know unless stated explicitly.

    Uh oh. Should have


    void* operator new( size_t size, myFunAllocator const& a )
    {
    ...
    }


    to match the usage and restrict client code to The Right Way.

    Now I remember both that there is no placement delete call syntax, and that
    old MFC had a bug where placement new was defined without definining a
    corresponding placement delete, resulting in a memory leak in debug builds.
    Checking... Oh yes, if the constructor throws during a placement new call
    then placement delete is called with the same "placement" args to
    deallocate, and this was where the MFC bug was -- so you also need


    void operator delete( void*, myFunAllocator )
    {
    // Used automatically when the constructor throws, §5.3.4/20.
    }


    as well as
    I don't know; since I don't use this (have only written allocators in
    PL/M-86, assembly and Pascal, never C or C++... ;-)) I'm no expert.
     
    Alf P. Steinbach, Aug 16, 2004
    #6
  7. Well, you somehow need to remember the type of the object.

    My suggestion is the following:

    // Basically calls the destructor.
    // It is assumed that p is of type T*
    template<class T>
    void CallDestructor(void* p)
    {
    static_cast<T*>(p)->~T();
    }
    typedef void (*DestructPtr)(void*);

    // and then replace the placement new with

    template<class T>
    T* MyNew(void* mem)
    {
    // Store a pointer to our "destructor"
    *static_cast<DestructPtr*>(mem) = CallDestructor<T>;

    // Calculate offset to place the object behind the pointer
    // TODO: ensure that *newMem is aligned properly!
    void* newMem = static_cast<DestructPtr*>(mem) + 1;

    // Placement-new the new object as usual
    return new(newMem) T;
    }

    void MyDelete(void* mem)
    {
    // Now you would call the destructor as follows
    (*static_cast<DestructPtr*>(mem))(mem);
    }


    Note that it is still necessary that you remember a pointer to the
    originally allocated object (which can be different, e.g. in the
    presence of multiple or virtual inheritance)

    It is possible to store this information in a smart-pointer, however.
    For example boost::shared_ptr, where you can pass a functor that can do
    additional cleanup when the memory is to be deleted (i.e. this functor
    could then store a pointer to the original memory and call MyDelete
    automatically).
    Using a smart pointer seems to be the most reliable way IMHO.

    Another approach might be to use thunks (see
    http://www.pluralsight.com/articlecontent/cpprep0399.htm for an
    example), but that is all very low-level and highly platform dependent.

    You could as well derive your object from a special base class that
    knows the destructor (works basically like the first approach, but is
    not so low-level and more obvious. I guess I should have mentioned this
    version first ;) )

    struct Deletable
    {
    virtual void CallMyDestructor() = 0;
    };
    template<class T>
    struct PlacementDeletable : public Deletable, public T
    {
    virtual void CallMyDestructor() { this->~T(); }
    }

    Now you have to store a Deletable* somewhere (or you could use
    dynamic_cast to get it) and call the destructor through that object.
     
    =?ISO-8859-1?Q?Tobias_G=FCntner?=, Aug 16, 2004
    #7
  8. Giancarlo Niccolai

    Max M. Guest

    GCC does indeed call B's destructor, though. I've always thought this
    behaviour was standard (and I am, in fact, relying on it in a few places).
    Too bad. It doesn't look very logical, though, does it?

    Max
     
    Max M., Aug 16, 2004
    #8
  9. Giancarlo Niccolai wrote in in comp.lang.c++:
    Why ? / Who says this ?

    Note Standard C++ has no concept of "Vtable", this is important as the
    symantics of virtual destructors (or member-functions or base-classes)
    are *not* defined in terms of virtual tables.

    #include <new>
    #include <cstdio>

    using std::printf;

    struct A
    {
    virtual ~A() { printf( "~A()\n" ); }
    };

    struct B: A
    {
    virtual ~B() { printf( "~B()\n" ); }
    };

    int main()
    {
    char good_memory[100];
    A *mem = new ((void *)good_memory) B();

    mem->~A();
    }

    On Every compiler I tried (*):

    output:
    ~B()
    ~A()

    *) MSVC 7.1, g++ 3.4 and 3.2 and CBuilderX 6.0 (EDG/preview).

    Rob.
     
    Rob Williscroft, Aug 16, 2004
    #9
  10. * Rob Williscroft:
    Cast is not necessary.
    $12.4/12 lists just about the same example and requires the output
    given above, so it is mandated by the Holy Standard (one interesting
    tidbit, given "typedef B B_alias;" the call b_ptr->~B_alias() is
    valid while b_ptr->B_alias::~B_alias() is invalid...).

    I didn't know that you get a virtual call here (I don't use this), but still
    recommend using member operator new and delete instead of direct placement
    new, mainly because a class should encapsulate correct usage.
     
    Alf P. Steinbach, Aug 16, 2004
    #10
  11. Actually, it doesn't, as ~A() semantically means exactly "call the method
    named ~A(), which is accidentally A class destructor". As B() class
    destructor is called ~B(), and not ~A(), even if they share the same Vtable
    entry, they have actually two different symbolic name. Differently, if a
    thing like

    mem->~mem();

    existed, which did not call the destructor by its symbol, but by its Vtable
    entry, then the thing would logically work, but p->~A() is semantically
    bound to call the function ~A() which is btw the A class destructor.

    That was the root of my doubts; how to call the destructor by its vtable
    entry, and not by its symbol-class name...

    Anyhow the delete operator overloading proposed by Victor (thanks again)
    works just fine;

    delete p;

    if p has a virtual destructor AND a static delete() operator declared in its
    class, will call the virtual destructor AND THEN free the memory using the
    code you feed in delete operator, exactly as wished.

    Giancarlo.
     
    Giancarlo Niccolai, Aug 16, 2004
    #11
  12. Ok, I buy your point; but what troubles my mind is "why the book(s) says
    that the only way to kill an object that you created with new(p) is to call
    its destructor explicitly?". In other words, why they do not suggest about
    the delete operator overloading? For "the books" I mean current literature
    and ALL the source I have come across, including BS manual, in which it is
    explicitly said not to use any other method than

    p->~A();
    yourFuncToFreeMem(p);

    in this case.

    A delete overload operator, as you correctly suggested, seems the logically
    and semantically right solution based on the C++ language definition. And
    moreover, it works. Then WHY the book(s) (read: official literature) does
    not only not mention it, but also strongly encourage another, less C++ish
    solution?

    Probably the issue is not even worth to be dug, the fact that virtual
    destructor + delete overloading does it fine should be enough, yet I ask if
    this is just a hole in the current literature, a missing spot, a frogotten
    topic, or if there is a deeper reason why BS and all the following official
    sources explicitally say that the only right way to cause the termination
    of a placed-new allocated item is the explicit destructor call + memory
    management function call.

    Giancarlo.
     
    Giancarlo Niccolai, Aug 16, 2004
    #12
  13. Alf P. Steinbach wrote:

    Yes, in fact I said "it works *also*", meaning "a suboptimal way, but even
    if suboptimal, working anyhow"...
    Ok. And what about classes that are NOT derived from MFC, so that you are
    sure that their constructor does not throw? (again, I share with you the
    point that the Right Thing is to feed also the overload new operator).

    The compiler should generate exactly the same code if i say

    new(builder) thing;

    with thing without a new overloading, but a global overload for "builder"
    AND

    new thing;

    with new overloaded in thing and using "builder" in the overloaded body;

    The fact that I am concerned about that is because I may want to change the
    builder object in the "new" operator invocations; if I did it by I.e.
    overloading the new operator as a static instance in "thing",this second
    solution would immediately become less elegant (and even risky) by far than
    the global overload by class type and the explicit new(object) call. So, as
    this two solution are sintactically different but semantically equivalent,
    I am concerned about the compiler generating the same or compatible code.

    Giancarlo.
     
    Giancarlo Niccolai, Aug 16, 2004
    #13
  14. Tobias Güntner wrote:

    <rip>

    It is exactly the solution I found before today, and it gives exactly the
    problem I reported. As you can see, template expansion of the ~T() call
    will call the destructor associated with the class of the POINTER you pass
    to CallDestructor(), and not the class of the OBJECT you pass it. So (using
    template<class T> void CallDestructor( T*p) instead of void * helps the
    compiler a little):

    class B: public A {
    ...
    };

    A* obj = new(mem) B;
    B* obj1 = new(mem) B;

    ....
    CallDestructor( obj ); // resolves in template A, calling p->~A;
    CallDestructor( obj1 ); // resolves in class B

    or, maintaining the void * as parameter,
    CallDestructor< ?what here? you have do decide at compile time>( obj );

    and what is in obj, if it can dynamically change at runtime?

    The functor is a good solution, but then you don't call the destructor
    anymore. This means that stack-allocated object cannot be automatically
    unwinded by the compiler. You have to allocate all the items in the heap,
    as a smart pointer would not be able to tell a heap-allocated item from a
    stack allocated one, while the overloading of the delete operator is.
    Uhm; first of all, you may not want all the objects in your application to
    be virtual. For really high-speed aps, as the vtable and the objects are
    unlikely to be kept near in memory, the memory cache flushes to access the
    vtable and then the object repeatedly may cause an unacceptable operational
    downgrade. Secondly, it does not cure the problem, as the problem is
    EXACTLY that of being unable to virtualize the destructor due to a lack in
    C++ grammar definition: you can call every method by its object-specific
    representation EXCEPT for the destructor, which you may call only with its
    CLASS specific representation. You may call a->method(), and method is
    relative to a object, so the compiler has to find a way to address it: is
    it a vtable entry? is it an inline method? what kind of pointer is A? can
    it hold virtual objects?. But with destructors, you may call only
    a->~CLASSNAME(); and then you reference the classname explicitly, the
    compiler won't try to understand what A may be except for that class
    instance. The only way to call a destructor by object instance specific
    representation is to use delete, but yet delete does not only call the
    destructor, but also free the memory... that's the lock-in situation I was
    facing; overloading delete seems the only possible solution, and actually,
    it worked, but then WHY, is my question, WHY this possibility is explicitly
    excluded by the literature about the placed new() operator?

    Giancarlo.
     
    Giancarlo Niccolai, Aug 16, 2004
    #14
  15. Giancarlo Niccolai

    Max M. Guest

    Not sure we're understanding each other. I meant to say I find GCC's
    behaviour to be more logical than that of your compiler, not the other way
    around. At any rate, as Alf P. Steinbach just pointed out, the Standard is
    clear on this. GCC is conformant in this respect; your compiler is not.
    See 12.4.12:

    [Example:
    struct B {
    virtual ~B() { }
    };
    struct D : B {
    ~D() { }
    };
    D D_object;
    typedef B B_alias;
    B* B_ptr = &D_object;
    D_object.B::~B(); // calls B's destructor
    B_ptr->~B(); // calls D's destructor
    B_ptr->~B_alias(); // calls D's destructor


    Max
     
    Max M., Aug 16, 2004
    #15
  16. because ~A() reference a CLASS NAME SYMBOL, and not a method name. So, the
    compiler know you are willing to call ~A(), even if ~A() is virtual and
    there is a ~B() overloading ~A(). With this construct, the language writers
    are assuming that the programmer know before compile time the class from
    which the object pointed by mem is instantiated from.
    Correct: I expressed myself in terms of implementations rather than in terms
    of abstraction; but the fact is that vtables implement virtuality in a 1:1
    semantic mapping. Virtuality is perfectly represented by vtables, which are
    the perfect structure to represent virtuality, that's why I feel confident
    in using vtables as a term to specify the compiler view of the virtuality
    definitions.
    GCC fails in that. Then is a GCC Issue? Or is a MSVC issue, as it calls ~B()
    even when the programmer explicitly calls ~A(). If I call mem->A::method(),
    I expect the method from A to be called, even if it's overloaded; calling
    the ~() method from class A is not semantically equivalent to call
    mem->A::method()?

    Giancarlo.
     
    Giancarlo Niccolai, Aug 16, 2004
    #16
  17. Ahh, here is the point: $12.4/12. So ~A() is a special case where using the
    class name is NOT semantically equivalent to access the class instance by
    name (see my other letters). So, I suppose that the problem lies in GCC,
    that fails in cope with this standard.

    That is then the reason why
    mem->~A();
    freeFunc(mem);

    is the *recommended* procedure to destroy an item allocated with mem(p); if
    ~A() is semantically treated not as a class name symbol reference, but as a
    possibly virtual object method, then all is clear.

    Thanks all for your kind explanations.

    Giancarlo.
     
    Giancarlo Niccolai, Aug 16, 2004
    #17
  18. Ok, Then the problem is that GCC fails if:

    template <class C>
    func( C *p ) {
    .....
    p->~C();
    }

    As you can see, GCC should do as

    B_ptr->~B(); // calls D's destructor

    but it doesn't. Eventually, then it seems to be a GCC bug in call of
    destructor from templates, in which it acts as if:

    template <class C>
    func( C *p ) {
    .....
    p->C::~C();
    }

    Instead of the other way (using virtual destructors).

    Giancarlo.
     
    Giancarlo Niccolai, Aug 16, 2004
    #18
  19. Alf P. Steinbach wrote in in
    comp.lang.c++:

    struct UDT {};
    void operator new ( std::size_t, UDT * )
    {
    return 0;
    }
    UDT good_memory[ 100 ];
    Yep. Thanks that was nit-ilitious :).

    Rob.
     
    Rob Williscroft, Aug 16, 2004
    #19
  20. Giancarlo Niccolai

    Kai-Uwe Bux Guest


    Now, how did you get that idea:

    #include <iostream>

    class Base {
    public:

    int x;

    virtual ~Base ( void ) {
    std::cout << "destructing Base" << std::endl;
    }

    };

    class Derived : public Base {
    public:

    int y;

    virtual ~Derived ( void ) {
    std::cout << "destructing Derived" << std::endl;
    }

    };


    template <class C>
    void func( C *p ) {
    p->~C();
    }

    int main ( void ) {

    Base* bp = new Derived();
    func( bp );

    }


    Output [ compiled with g++ (GCC) 3.4.0 ] :

    destructing Derived
    destructing Base



    Best

    Kai-Uwe Bux
     
    Kai-Uwe Bux, Aug 16, 2004
    #20
    1. Advertisements

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments (here). After that, you can post your question and our members will help you out.