3

I ran across this issue in my application after checking it for memory leaks, and discovered that some of my classes are not being destroyed at all.

The code below is split into 3 files, it is supposed to implement a pattern called pimpl. The expected scenario is to have both Cimpl constructor and destructor print their messages. However, that's not what I get with g++. In my application, only constructor got called.

classes.h:

#include <memory>

class Cimpl;

class Cpimpl {
    std::auto_ptr<Cimpl> impl;
public:
    Cpimpl();
};

classes.cpp:

#include "classes.h"
#include <stdio.h>

class Cimpl {
public:
    Cimpl() {
        printf("Cimpl::Cimpl()\n");
    }
    ~Cimpl() {
        printf("Cimpl::~Cimpl()\n");
    }
};    

Cpimpl::Cpimpl() {
    this->impl.reset(new Cimpl);
}

main.cpp:

#include "classes.h"

int main() {
    Cpimpl c;
    return 0;
}

Here is what I was able to discover further:

g++ -Wall -c main.cpp
g++ -Wall -c classes.cpp
g++ -Wall main.o classes.o -o app_bug
g++ -Wall classes.o main.o -o app_ok

It looks like the destructor is being called in one of two possible cases, and it depends on the linking order. With app_ok I was able to get the correct scenario, while app_bug behaved exactly like my application.

Is there any bit of wisdom I am missing in this situation? Thanks for any suggestion in advance!

Tosha
  • 998
  • 1
  • 10
  • 22
  • There are some situations when destructors are not called. [This answer](http://stackoverflow.com/a/3179812/1283847) will be helpful. – Leri Sep 07 '12 at 17:59
  • 1
    `std::auto_ptr` tries to call `Cimpl`'s destructor which is not declared in "classes.h". I'm not sure what the standard requires for this situation, but you can solve this problem by either deriving `Cimpl` from a base class with a virtual destructor and working with a base class pointer, or by deleting the implementation instance manually. – Fabian Knorr Sep 07 '12 at 18:02
  • I strongly recommend you read [this question](http://stackoverflow.com/questions/8595471/does-the-gotw-101-solution-actually-solve-anything) and the blog posts it discusses. All the relevant issues are covered. – Ben Voigt Sep 07 '12 at 18:39

4 Answers4

1

The goal of the pimpl idiom is to not have to expose a definition of the implementation class in the header file. But all the standard smart pointers require a definition of their template parameter to be visible at the point of declaration in order to work correctly.

That means this is one of the rare occasions where you actually want to use new, delete, and a bare pointer. (If I'm wrong about this and there's a standard smart pointer that can be used for pimpl, someone please let me know.)

classes.h

struct Cimpl;

struct Cpimpl
{
    Cpimpl();
    ~Cpimpl();

    // other public methods here

private:
    Cimpl *ptr;

    // Cpimpl must be uncopyable or else make these copy the Cimpl
    Cpimpl(const Cpimpl&);
    Cpimpl& operator=(const Cpimpl&);
};

classes.cpp

#include <stdio.h>

struct Cimpl
{
    Cimpl()
    {
        puts("Cimpl::Cimpl()");
    }
    ~Cimpl()
    {
        puts("Cimpl::~Cimpl()");
    }

    // etc
};

Cpimpl::Cpimpl() : ptr(new Cimpl) {}
Cpimpl::~Cpimpl() { delete ptr; }

// etc
zwol
  • 135,547
  • 38
  • 252
  • 361
  • 1
    `auto_ptr` does require a complete type at the point where it is instantiated. The new smart pointers, `shared_ptr` and `unique_ptr`, do not; but if you instantiate one of them on an incomplete type you have to provide a custom deleter that can handle the complete type correctly. – Pete Becker Sep 07 '12 at 18:57
  • I did not know that was a *requirement* for the new smart pointers. Do I understand you correctly that if it's okay to require C++11, the bare pointer could become `std::unique_ptr` and the out-of-line destructor could have an empty body, but the out-of-line constructor and destructor are still necessary? – zwol Sep 07 '12 at 19:16
  • IIRC the `unique_ptr` requires a complete type at the point of usage, but `shared_ptr` does not. Even they are different, I think, but I don't have the doc to back that up. I agree that `auto_ptr` needs a complete type, and is thus not appropriate for pmpl. – Kevin Anderson Sep 07 '12 at 20:07
  • @Zack - I said it too strongly: the default deleter for `unique_ptr` can handle an incomplete type, so you don't need to provide a custom deleter in that case. But, as with all the other member functions that deal with the managed object, at the point where you use it (i.e., at the point where a `unique_ptr` object is destroyed) the type must be complete. – Pete Becker Sep 07 '12 at 20:38
  • @Kevin - seems to me that the key difference between `auto_ptr` and `unique_ptr` in this regard is that `auto_ptr` must be **instantiated** on a complete type, but `unique_ptr` only needs a complete type at the point where it is destroyed (so that it can properly delete the managed object). – Pete Becker Sep 07 '12 at 20:39
1

The problem is that at the point of the definition of the auto_ptr<Cimpl> object, Cimpl is an incomplete type, that is, the compiler has only seen a forward declaration of Cimpl. That's okay, but since it eventually deletes the object that it holds a pointer to, you have to comply with this requirement, from [expr.delete]/5:

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.

So this code runs into undefined behavior, and all bets are off.

Pete Becker
  • 74,985
  • 8
  • 76
  • 165
0

The code violates the One Definition Rule. There's a definition of the class Cimpl in classes.h, and a different definition of the class Cimpl in the file classes.cpp. The result is undefined behavior. It's okay to have more than one definition of a class, but they must be the same.

Pete Becker
  • 74,985
  • 8
  • 76
  • 165
  • There is no definition of `Cimpl` in the header file, only a forward declaration. As noted in my answer though this *does* still result in a violation of the one definition rule. – Mark B Sep 07 '12 at 18:15
  • @MarkB - read the code you posted. There is a forward declaration, and it's followed by a definition. – Pete Becker Sep 07 '12 at 18:17
  • @MarkB - whoops, that wasn't the code that **you** posted. Sorry about that. – Pete Becker Sep 07 '12 at 18:23
  • You may have missed that the header forward declares `Cimpl` and then defines `Cpimpl` as the names look very similar. – Mark B Sep 07 '12 at 18:33
  • @MarkB - yup, I missed that. Thanks for pointing it out. The names look too much alike. – Pete Becker Sep 07 '12 at 18:36
  • @MarkB - please acknowledge seeing my retraction. Then I'll delete this message. – Pete Becker Sep 07 '12 at 18:49
0

Edited for clarity, original retained below.

This code has undefined behavior because in the context of main.cpp the implicit Cpimpl::~Cpimpl destructor only has a forward declaration of Cimpl, but the auto_ptr (or any other form of doing delete) needs a full definition to legally clean up the Cimpl. Given that it's undefined behavior no further explanation for your observations is needed.

Original answer:

I suspect that what's happening here is that the implicit destructor of Cpimpl is being generated in the context of classes.h and not having access to the full definition of Cimpl. Then when the auto_ptr tries to do its thing and clean up its contained pointer, it deletes an incomplete class, which is undefined behavior. Given that it's undefined we don't have to go any further to explain that it's perfectly acceptable for it to work in different ways depending on the link order.

I suspect that an explicit destructor for Cpimpl with a definition in a source file would solve your problem.

EDIT: Actually now that I look at it again, I believe your program violates the one definition rule as it stands. In main.cpp it sees an implicit destructor that doesn't know how to call a destructor of Cimpl (because it only has a forward declaration). In classes.cpp the implicit destructor does have access to Cimpl's definition and thus how to call its destructor.

Mark B
  • 95,107
  • 10
  • 109
  • 188
  • Re: "sees an implicit destructor...", etc. That's all well and good, but undefined behavior is undefined behavior. Nothing more. There's no point in trying to analyze what the compiler did in a case like this. – Pete Becker Sep 07 '12 at 18:25
  • Formally, the problem isn't an ODR violation, but a violation of a particular prohibition for the `delete` operator. It's okay to delete a pointer to an incomplete type, but if that type in fact has a destructor, the behavior is undefined. – Pete Becker Sep 07 '12 at 18:53