3

I am confused on why the destructor calls itself an endless amount of times, when I try to construct an object say LeakySingleton on the heap through a static function call create_instance() and then try to delete it explicitly afterwards via the delete operation.

As I understand it, considering the source listings from below, the variable leaky_singleton inside main() gets to point to the heap allocated resource returned by create_instance(). Thus, we have allocated an object LeakySingleton on the heap indirectly through the create_instance function. Now, if I explicitly call the delete operator or delete function on leaky_singleton, then it calls the destructor first and checks whether or not it satisfies the instance != nullptr condition and then the deletion of the object that instance points to should be deleted. If this object LeakySingleton::instance is deleted, then there is no reason for the dtor to call itself again, or am I missing something here?

Calling it with and without valgrind leads to a segmentation fault (invalid memory access due to stack overflow):

Segmentation fault (core dumped)

Stepping through with the debugger, leads to endless destructor calls (the culprit of the stack overflow).

From cplusplus.com (http://www.cplusplus.com/forum/general/40044/):

If you delete your object, it attempts to delete itself, which will cause it to attempt to delete itself, which will cause it to delete itself, which will...

Why does it attempt to delete itself, when I simply use the delete operator/function to deallocate the heap object LeakySingleton pointed to by the static class member variable LeakySingleton::instance? The heap allocated resource is pointed to by the LeakySingleton::instance pointer variable which points to a LeakySingleton object. So why does an explicit delete function call doesn't delete or rather deallocate the allocated heap object and instead recurses endlessly? What am I missing here?

(My current dtor and ctor understanding: The new function/operator allocates memory for an object on the heap and calls the constructor, and the delete function calls the destructor and in my case also calls a delete operator/function inside.)

Source:

main.cpp:

class Singleton final
{
    public:
        static Singleton & create_instance(int);
        ~Singleton() = default;
    private:
        int x;
        Singleton(int);

        Singleton(Singleton &) = delete;
        Singleton(Singleton &&) = delete;
        Singleton & operator=(Singleton &) = delete;
        Singleton & operator=(Singleton &&) = delete;
};

Singleton::Singleton(int t_x) : x{t_x}
{}

Singleton & Singleton::create_instance(int t_x)
{
    static Singleton instance{t_x};
    return instance;
}

// Potential endless dtor calls inside:
class LeakySingleton final
{
    public:
        static LeakySingleton * create_instance(int);
        ~LeakySingleton();
    private:
        int x;
        static LeakySingleton * instance;
        LeakySingleton(int);

        LeakySingleton(LeakySingleton &) = delete;
        LeakySingleton(LeakySingleton &&) = delete;
        LeakySingleton & operator=(LeakySingleton &) = delete;
        LeakySingleton & operator=(LeakySingleton &&) = delete;
};

LeakySingleton * LeakySingleton::instance = nullptr;

LeakySingleton::LeakySingleton(int t_x) : x{t_x}
{}

LeakySingleton::~LeakySingleton()
{
    if (instance != nullptr)
    {
        delete instance;
        instance = nullptr;
    }
}

LeakySingleton * LeakySingleton::create_instance(int t_x)
{
    if (instance == nullptr)
    {
        instance = new LeakySingleton{t_x};
    }
    return instance;
}

int main()
{ 
    // The correct implementation with no issues:
    {
        Singleton & singleton = Singleton::create_instance(42);
    }

    // The faulty implementation causing the dtor to recurse endlessly and resulting in a segfault:
    {
        LeakySingleton * leaky_singleton = LeakySingleton::create_instance(42);
        delete leaky_singleton;
    }

    return 0;
}

Makefile:

CC = g++
CFLAGS = -g -Wall -Wextra -pedantic -std=c++11
SRC = main.cpp
TARGET = app
RM = rm -rf

.PHONY: all clean

all: $(TARGET)

clean:
    $(RM) $(TARGET)

$(TARGET): $(SRC)
    $(CC) $(CFLAGS) $^ -o $@
Christophe
  • 68,716
  • 7
  • 72
  • 138
  • 1
    delete calls dtor, which calls delete, which clls dtor, ... You don't set instance to nullptr until after delete returns (which it never will). – stark Nov 10 '19 at 23:41
  • @stark, thanks for the input, but I am still confused on why this is. When the dtor gets called, delete is issued so far so good, but why does the delete cause another dtor call? – user12197465 Nov 10 '19 at 23:45
  • When you call new, it allocates memory, then calls the ctor. When you call delete, it first calls the dtor, then frees the memory. – stark Nov 10 '19 at 23:48
  • Yes, I can follow you so far, but then why does it get called multiple times? If you consider this simple source listing, then dtor gets called only once: https://privatebin.net/?3987daf23e757f44#5dauBj8nAdAcqYc4kuuSQVV3qNtH1kN6XEg7ZzGpDBGJ – user12197465 Nov 10 '19 at 23:54
  • 1
    instance == this here. You first call delete (which calls the destructor) and then nullify the pointer - so at each call the pointer is not null [yet]. Also, is there a reason you're not using a typical singleton implementation with function-level static variable? – Sopel Nov 11 '19 at 00:00
  • @Sopel Am I essentially doing a 'delete this'?: https://stackoverflow.com/questions/550189/is-it-safe-to-delete-this – user12197465 Nov 11 '19 at 02:29
  • If I understand amon correctly, then a 'delete this' causes infinite dtor calls?: https://softwareengineering.stackexchange.com/questions/298882/using-delete-this-to-free-memory-in-class – user12197465 Nov 11 '19 at 02:36
  • If essentially what I am doing is a 'delete this', then my entire question boils down to: 'why does a delete this cause a dtor to recurse endlessly?' – user12197465 Nov 11 '19 at 02:41

4 Answers4

2

You have a nasty cycle, in LeakySingleton::create_instance you have:

instance = new LeakySingleton{t_x};

then in LeakySingleton's destuctor you have:

delete instance;

which is going to call LeakySingleton's destuctor before you set anything to null:

instance = nullptr;

so you have infinte recursion causing your stackoverflow.

Paul Evans
  • 27,315
  • 3
  • 37
  • 54
2

In C++, delete will call the class destructor.

The delete statement in your main function is calling LeakySingleton::~LeakySingleton, which in turn tries to delete the static instance pointer, which then calls the destructor again. Your code never had the chance to set the static pointer to null. There you have an infinite loop.

P.S. IMHO, it is generally a bad practice to modify static members in non-static methods. I believe you can put the static clean up logic in another static method.

class LeakySingleton final {
public:
  static LeakySingleton& create_instance(int);
  static void destroy_instance();
  ~LeakySinglton() = default;
private:
  static LeakySingleton *instance;
  ...
};

void LeakySingleton::destroy_instance() {
  if (instance != nullptr) {
    delete instance;
    instance = nullptr;
  }
}
xiaofeng.li
  • 8,237
  • 2
  • 23
  • 30
  • Thank you for your input @xiaofeng.li, but I am still unsure on why it calls the dtor again after it "tries" to delete the static instance pointer. Here the dtor gets called once: https://privatebin.net/?3987daf23e757f44#5dauBj8nAdAcqYc4kuuSQVV3qNtH1kN6XEg7ZzGpDBGJ – user12197465 Nov 11 '19 at 00:25
  • @user12197465 -- Your example is not `deleting` an object of type `A`. Therefore it is not similar or even close to the issue you're seeing. – PaulMcKenzie Nov 11 '19 at 00:55
  • @PaulMcKenzie Ah, true. So it has something to do with its self referencing behavior. The comment is apparently deleted, but the person commenting was hinting me that it is due to 'this == LeakySingelton::instance' if I recall correctly. So, I am deconstructing "this" - the object itself? To clarify: I am calling 'new' inside a static function block, new allocates me memory for 'LeakySingleton', so far so good. Then, I call dtor, dtor calls delete on 'LeakySingleton::instance' and for some reason it gets called multiple times. This is what I don't understand exactly: the multiple calls. – user12197465 Nov 11 '19 at 01:23
  • *the comment by Sopel is still there. It isn't deleted as I mistakingly assumed. – user12197465 Nov 11 '19 at 01:26
  • I think, I made myself a stumbling block that I cannot identify by myself yet. I am apparently having false assumptions which inhibit my understanding of all the inputs provided by all the friendly people on here. Or, I am simply ignoring something that is key for my understanding. But I am unsure what it is exactly that keeps me from getting it. Maybe pausing on it and reflecting on it helps? I will try to read and reread the answers & comments again and again and hopefully it clicks someday. :/ – user12197465 Nov 11 '19 at 01:42
  • 1
    I think I finally understand it, since I am doing a 'delete this' inside a dtor, and 'this' is a ptr to an object, the delete invokes the object's dtor again, hence it never ends, since a 'delete Object' construction always will invoke its dtor. Thank you all! :) – user12197465 Nov 11 '19 at 21:40
0

Delete instace in destructor initiate a destructor call.

M.Hefny
  • 2,677
  • 1
  • 26
  • 30
0

First since the LeakySingleton shall not be created directly, it shall not be destroyed directly either:

  • Therefore its destructor should be private, exactly as its constructor.
  • if the singleton may be deleted: it shall be deleted with a public function delete_instance() that deletes the instance
  • the destructor shall not delete itself (endless rescursion)
  • this construct should avoid this endless destructor recursion

If you want the instance pointer to leak and allow its destruction, you shall not do it twice (once outside and once inside the destructor), but only once outside the destructor. Since there is only one instance the destructor outside means that there is no need for a delete call inside:

LeakySingleton::~LeakySingleton()
{
    if (instance != nullptr)
    {
         instance = nullptr;  // since there's only one, it's the instance and
    }                         // the instance pointer shall be reset
    // and do what's needed to clean the object
}   

Note: this implementation is not thread-safe.

Note 2: this article could be of interest to you. It also warns against public destructor, since this could lead to dangling pointers.

Christophe
  • 68,716
  • 7
  • 72
  • 138