Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why does incomplete type of smart pointer data member and raw pointer data member have different behavior when their parent destruct?

In the following code:

smart pointer data member pImpl(class Impl) and raw pointer pc(class CAT) all are incomplete data type, there is no definition of these two classes in Widget.h

//widget.h

#ifndef W_H_
#define W_H_
#include <memory>

class Widget { 
    public:
        Widget();
        ~Widget() {    //
            delete pc; // I know I should put ~Widget to .cpp
                       // I just want to show the difference in behavior
                       // between raw pointer and smart pointer(both has incomplete type)
                       // when widget object destructs 
        }
    private:
        struct Impl; 
        std::shared_ptr<Impl> pImpl;  // use smart pointer
        struct CAT;
        CAT *pc;  //raw pointer
};
#endif

//widget.cpp

#include "widget.h"

#include <string>
#include <vector>
#include <iostream>
using namespace std;
struct Widget::CAT 
{
    std::string name;
    CAT(){cout<<"CAT"<<endl;}
    ~CAT(){cout<<"~CAT"<<endl;}
};      


struct Widget::Impl {
    std::string name;
    Impl(){cout<<"Impl"<<endl;}
    ~Impl(){cout<<"~Impl"<<endl;}
};


Widget::Widget()  
    : pImpl(std::make_shared<Impl>()),
      pc(new CAT)
{} 

//main.cpp

#include "widget.h"
int main()
{
    Widget w;
}

//output

Impl

CAT

~Impl

For the raw pointer data member, its destuctor is not called when widget object is destructed.

While the shared_ptr data member, its destructor has been correctly called.

To my understanding, in Widget::~Widget() it should generate some code automatically as the following:

        ~Widget() {
            delete pc; // wrote by me
            
            // generated by compiler
            delete pImpl->get();
        }

Why do shared_ptr data member and raw data member have different behavior when the widget gets destructed?

I test the code using g++4.8.2 in Linux

================================EDIT=============================== According to the answers, the reason is because of :

the code generated by compiler is NOT:

        ~Widget() {
            delete pc; // wrote by me
            
            // generated by compiler
            delete pImpl->get();
        }

it maybe something like:

        ~Widget() {
            delete pc; // wrote by me
            
            // generated by compiler
            pimpl.deleter(); //deleter will be initailized while pimpl object is initialized
        }
like image 758
camino Avatar asked Mar 15 '15 15:03

camino


2 Answers

Because you forward declaration "CAT" in the header file, you have an incomplete data type. With this information the compiler fall into undefined behavior and the destructor may not be called.

What the standard says

if the object being deleted has incomplete class type at the point of deletion and the complete class has a non-trivial destructor or a deallocation function, the behavior is undefined.

Here you can find a detailed explanation: Why, really, deleting an incomplete type is undefined behaviour? Just moving the struct declaration to before the class definition should fix your problem:

struct CAT
{
    std::string name;
    CAT(){std::cout<<"CAT"<<std::endl;}
    ~CAT(){std::cout<<"~CAT"<<std::endl;}
};

class Widget {
    public:
        Widget();
        ~Widget() {
            delete pc; // I know we should put this code to cpp
                       // I am just want to show the difference behavior
                       // between raw pointer and smart pointer
                       // when widget object destruct
        }
    private:
        struct Impl;
        std::shared_ptr<Impl> pImpl;  // use smart pointer
        CAT *pc;  //raw pointer
};

And the output

Impl
CAT
~CAT
~Impl

Forward declarations are good to speed up compilation time, but can lead to problems when more information about the data type is needed.

But why does it work for smart pointers? Here's a better explanation: Deletion of pointer to incomplete type and smart pointers

Basically, the shared_ptr only needs the declaration when it initializes or resets the pointer. That means it doesn't need the complete type on the moment of the declaration.

This functionality isn't free: shared_ptr has to create and store a pointer to the deleter functor; typically this is done by storing the deleter as part of the block that stores the strong and weak reference counts or by having a pointer as part of that block that points to the deleter (since you can provide your own deleter).

like image 99
dfranca Avatar answered Nov 06 '22 13:11

dfranca


Explanation

You are trying to delete an object of incomplete type, this is undefined behavior if the object type being deleted has a non-trivial destructor.

More about the matter can be read about in [expr.delete] in the Standard, as well as under the following link:

  • stackoverflow.com - Why, really, deleting an incomplete type is undefined behaviour?

Note: The destructor of Widget::Cat is non-trivial since it is user-declared; in turn this means it is not called.



Solution

To fix the problem simply provide the definition of Widget::Cat so that it's not incomplete when you do delete pc.



Why does it work for shared_ptr?

The reason it works when using a shared_ptr is that the "point of deletion" doesn't happen until you actually construct the shared_ptr instance (through make_shared; ie. when the Deleter is actually instantiated.

like image 23
Filip Roséen - refp Avatar answered Nov 06 '22 13:11

Filip Roséen - refp