29 Nisan 2020 Çarşamba

Destructor metodu

Giriş
Derleyici biz destructor yazmasak bile "implicit declared" bir destructor üretir. Kendimiz boş bir tane yazarsak bu "user declared" olarak adlandırılıyor.

Bir sınıfın her zaman heap'te yaratılmasını "delete"edilmemesini istiyorsak, sınıfa pure virtual bir destructor tanımlayabiliriz.

Destructor Sırası
Sıra şöyle
1. Nesnenin destructor'ı çağrılır.
2. Üye nesnelerin destructor metodları çağrılır.
3. Ata sınıfın destructor metodu çağrılır.

Üye nesneler ya da arka arkaya yaratılan nesneler ters sırada yok edilir. Önce s5 sonra s4 nesnesinin destructor metodu çağrılır.
Student s1;
Student s2;
Student s3;
Student s4;
Student s5;
Deleted Destructor
Nesne delete edilemez. Derleyici hata verir. Elimizde şöyle bir kod olsun.
struct A
{
    ~A() = delete;
};
Eğer bu sınıfı new'leyip sonra delete edersek şöyle bir hata alırız.
error: use of deleted function 'A::~A()' new A{};
Destructor Virtual Olmalı mı ?
Aslında virtual olmak zorunda değil. Elimizde şöyle bir kod olsun.
class B {
  public :
    ~B() { // not virtual
      ...
    }
  ...    
}

class D : public B {
  public :
    ~D() {
      ...
    }
  ...
}
Eğer D stack'te yatarılırsa B'nin destrcutor'ı çağrılır. Sorun çıkmaz
D d;
Eğer D heap'te yatarılırsa B'nin destrcutor'ı çağrılır. Sorun çıkmaz
D* p = new D ();
delete p;
Sorun polymorphism ile çıkıyor. Bu durumda sadece B'nin destructor'ı çağrılır.
B* p = new D ();
delete p;
Mevcut Destructor Virtual Yapılabilir mi ?
Bence rahatlıkla yapılabilir. Ancak destructor virtual yapılırsa sınıf artık .rodata alanında olamaz. Açıklaması şöyle.
The moment you declare a virtual method, you add a non-constant pointer to your class that points to the virtual table of that class. This pointer will first be initialized to Object's virtual table, and then continue to change to the derived classes' virtual pointers throughout the constructor chain. It will then change again during the destructor chain and roll-back until it points to Object's virtual table. That would mean that your object can no longer be a pure read-only object and must move out of .rodata.
Destructor Elle Çağrılmasa İyi Olur
Stack'te yaratılan nesneler için dikkatli olmak lazım, çünkü nesne scope'tan çıkınca destructor zaten çağrılacak. Açıklaması şöyle.
Once a destructor is invoked for an object, the object no longer exists; the behavior is undefined if the destructor is invoked for an object whose lifetime has ended ([basic.life]). [ Example: If the destructor for an automatic object is explicitly invoked, and the block is subsequently left in a manner that would ordinarily invoke implicit destruction of the object, the behavior is undefined. — end example  
Örnek
Şu kod yanlış.
class First
{
  ...
};

int main()
{
  First FirstObject;
  FirstObject.Print();
  FirstObject.~First();
}
Destructor'ın Adresi
Destructor'ın adresi alınamaz. Açıklaması şöyle.
A destructor is used to destroy objects of its class type. The address of a destructor shall not be taken. 
Örnek
Şu kod foo nesnesinin destructor metodunu alıp çağırmaya çalışıyor ancak hatalı
void (*p)() = foo.~Foo;
Örnek
Şöyle yaparız.
template<class T>
void destruct(const T* x) {
    x->~T();
}
Kullanmak için şöyle yaparız.
td::bind(&destruct<Foo>, foo_ptr);
Destructor İçinde Yapılmaması Gerekenler
Destructor içinde virtual metodlar kullanılamaz! Ancak this-> kelimesi kullanılabilir. Ayrıca Destructor hiç bir zaman exception atmamalıdır!

Destructor + Heap ve Exception
Destructor içinde exception atılsa bile yani destructor metodu yarım kalsa bile nesnenin destructor metodunun başlaması yok olması için yeterlidir.
The lifetime of an object of type T ends when:
— if T is a class type with a non-trivial destructor (12.4), the destructor call starts
Örnek
Şöyle bir örneğe bakalım.
#include <csignal>

class A
{
public:
    virtual ~A() {}
    virtual void foo() = 0;
};

class B : public A
{
public:
    virtual ~B() { throw 5; } 
    virtual void foo() {}
};

int main(int, char * [])
{
    A * b = new B();

    try
    {
        delete b;
    }
    catch ( ... )
    {
        raise(SIGTRAP);
    }
    return 0;
}
B nesnesinin destructor metodu exception atıyor. catch içinde bu exception yakalanıp nesneye bakarsak sadece A'nın vtable satırları görülebilir. B destructor metodu bir kere çağrıldığı için artık yok olmuştur.
(gdb) i vtbl b
vtable for 'A' @ 0x400cf0 (subobject @ 0x603010):
[0]: 0x0
[1]: 0x0
[2]: 0x4008e0 <__cxa_pure_virtual@plt>
Örnek
Elimizde operator delete metodunu gerçekleştiren bir sınıf olsun.
class A
{
public:
  A() { }
  ~A() noexcept(false) { throw exception(); }
  void* operator new (std::size_t count)
  {
    cout << "hi" << endl;
    return ::operator new(count);
  }
  void operator delete (void* ptr)
  {
    cout << "bye" << endl;
    return ::operator delete(ptr);
  }
  // using these (with corresponding new's) don't seem to work either
  // void operator delete (void* ptr, const std::nothrow_t& tag);
  // void operator delete (void* ptr, void* place);
};

int main()
{
  A* a = new A();
  try
  {
    delete a;
  }
  catch(...)
  {
    cout << "eek" << endl;
  }
  return 0;
}
Teorik olarak operator delete metodunun destructor'ta exception fırlatılsa bile çağrılması gerekir. Ancak bazı derleyiciler böyle çalışmıyor.

Destructor + Stack ve Exception
Açıklaması şöyle. stack unwinding esnasında exception fırlatılırsa return edilen nesne veya geçici nesneler temizlenir
If an exception is thrown during the destruction of temporaries or local variables for a return statement, the destructor for the returned object (if any) is also invoked. The objects are destroyed in the reverse order of the completion of their construction.
Örnek - stack unwinding esnasında exception fırlatılırsa return edilen nesne temizlenir
Elimizde şöyle bir kod olsun. Y sınıfı destructor'ında exception fırlatsın. Y'den sonra yaratılan 100 değerine sahip A nesnesi de güzelce temizlenir.
struct A {
  int a_;
  explicit A(int a):a_(a) {
    printf("A(%d)'s ctor\n", a_);
  }
  ~A() {
    printf("A(%d)'s dtor\n", a_);
  }
};

struct Y { 
  ~Y() noexcept(false) {
    printf("y's dtor\n");
    throw 0;
  }
};

A f() {
  try {
    A a(23);
    Y y;
    A b(56);
    return A(100); // #1 who destructs this ??
  } catch (...) {
    printf("handling exception..\n");
  }
  printf("At line %d now..\n", __LINE__);
  return A(200); // #2
}

int main() {
  auto ret = f();
  printf("f returned A(%d) object\n", ret.a_);
  return 0;
}
Açıklaması şöyle.
after the Y destructor throws, the next thing that happens is that the A(100) object should be destroyed, so you should see a destruction message.
Örnek - stack unwinding esnasında exception fırlatılırsa geçici edilen nesne temizlenir
Elimizde şöyle bir sınıf olsun.
class A {
private: 
  int i;

public:
  A()
  {
    i = 10;
  }
  ~A()
  {
    throw 30;
  }
};
Bu sınıfı kullanmak için şöyle yaparız.
int main(){
  try{
    A a;      // begin `a` lifetime 
    throw 10; // | throw #0           
              // | end `a` lifetime   
                  // throw #1
  }
  catch(int i){
    cout<<i<<endl;
    cout<<"exception caught"<<endl;
  }
}
stack unwinding esnasında destructor çağrılır ve exception fırlatıldığı için std::terminate çağrılır.

Eğer sınıfı temporary olarak kullansaydık stack unwinding yapılmayacağı için sorun çıkmazdı.
int main(){
  try{
    A();
    throw 10;
  }
  catch (int i){
    cout << i << endl;
    cout << "exception caught" << endl;
  }
}
Çıktı olarak şunu alırız.
30
exception caught
C++11 ve Destructor
Açıklaması şöyle.
Any user-defined destructor is noexcept(true) by default, unless the declaration specifies otherwise, or the destructor of any base or member is noexcept(false).
C++11'den sonra destructor imzasında noexcept olduğu kabul edilir. Bu değiştirilmek istenirse şöyle yaparız.
~A() noexcept(false)
{
  throw 30;
}