Home > Software design >  polyBasePtr->~PolyBase() : is this really a virtual call?
polyBasePtr->~PolyBase() : is this really a virtual call?

Time:03-07

Since destructors are rarely called explicitly, I wonder if standard guarantees that calling explicitly a virtual destructor on a base class pointer will invoke a dtor of the most derived object. In cases I checked, this is indeed what happens in gcc, clang and msvc, but I would still like to know that it is really guaranteed.

To be honest, I would probably never asked this question, if the name of destructor was simply destructor() in every class. However, the call

basePtr->~Base();

looks a bit similar to

basePtr->Base::virtual_fcn();

which is not a virtual call. By contrast, usual way of invoking a dtor of the most derived object, through

delete basePtr;

does not provide a hint to a compiler that (maybe) we are requesting the base class version.

BTW, this is not a purely academic question. I want to have a container that can store a single object of a class derived from a given base with virtual dtor, up to some maximal size. If basePtr->~Base() is a virtual call, implementation of such a container is pretty easy and, only for completeness, shown below (sort of modeled after std::optional).

#include<cstddef>
#include<type_traits>
#include<new>
#include<utility>

template <typename PolyBaseT, std::size_t MaxSize>
class SubClassContainer{
  static_assert ( std::is_class_v<PolyBaseT> );
  static_assert ( std::has_virtual_destructor_v<PolyBaseT> );
  static_assert ( sizeof(PolyBaseT) <= MaxSize );
public:
  SubClassContainer() noexcept :m_basePtr(nullptr) {}
  void reset() noexcept { if(m_basePtr) {m_basePtr->~PolyBaseT(); m_basePtr=nullptr;} }
  ~SubClassContainer() noexcept { reset(); }

  template<typename DerivedT, typename ...ArgsT>
  DerivedT* emplace(ArgsT&& ...args)
  {  static_assert ( std::is_convertible_v<DerivedT*,PolyBaseT*> );
     static_assert ( std::is_constructible_v<DerivedT,decltype(std::forward<ArgsT>(args))...> );
     static_assert ( sizeof (DerivedT) <= MaxSize );
     static_assert ( alignof(DerivedT) <= alignof (std::max_align_t) );
     reset();
     auto result = ::new (static_cast<void*>(&m_storage[0]) ) DerivedT(std::forward<ArgsT>(args)... );
     m_basePtr=result;
     return result;
  }


  explicit operator bool () const noexcept {return m_basePtr;}
  const PolyBaseT * operator->() const  noexcept {return m_basePtr;}
  PolyBaseT * operator->()  noexcept {return m_basePtr;}

  SubClassContainer(const SubClassContainer&) = delete;
  SubClassContainer(SubClassContainer&&) = delete;
  SubClassContainer& operator=(const SubClassContainer&) = delete;
  SubClassContainer& operator=(SubClassContainer&&) = delete;

private:

  alignas(std::max_align_t) unsigned char m_storage[MaxSize];
  PolyBaseT *m_basePtr ;
};

#include <iostream>


struct B { virtual void id(){} virtual ~B() noexcept = default;};

struct D1:B{ virtual ~D1() {std::cout << __func__ <<std::endl;}  };
struct D2:B{ virtual ~D2() {std::cout << __func__ <<std::endl;}  };
struct D3:B{ D3(int) {}
             D3(double&&) {}
             virtual ~D3() {std::cout << __func__ <<std::endl;}
};


int main()
{

    SubClassContainer<B,128> der;

    der.emplace<D1>();

    std::cout << "about to reset: " << std::endl;
    der.emplace<D2>();

    std::cout << "about to reset:" << std::endl;

    der.emplace<D3>(7);
    std::cout << "about to reset [implicitely]" << std::endl;

}

CodePudding user response:

[class.dtor]/14 In an explicit destructor call, the destructor is specified by a ~ followed by a type-name or decltype-specifier that denotes the destructor’s class type. The invocation of a destructor is subject to the usual rules for member functions (12.2.1)
[ Example:

struct B {
  virtual ~B() { }
};
struct D : B {
  ~D() { }
};
D D_object;
B* B_ptr = &D_object;
void f() {
  D_object.B::~B(); // calls B’s destructor
  B_ptr->~B(); // calls D’s destructor
}

—end example ]

CodePudding user response:

Yes, it's a virtual destructor call. A non-virtual call would be basePtr->Base::~Base();.

Here's how you test it:

#include <iostream>

struct A
{
    virtual ~A()
    {
        std::cout << "~A()\n";
    }
};

struct B : A
{
    virtual ~B()
    {
        std::cout << "~B()\n";
    }
};

int main()
{
    A *p = new B;
    p->~A();
}
  • Related