auto_ptr for incomplete types

By Ali Çehreli

February 2002

I've been ignoring this issue since the first time I received the "deletion of pointer to incomplete type" warning from the compiler. The expensive approach I used to take to make the warning go away was to include the class header file to make the class definition visible to the compiler. [0]

There are many benefits of using smart pointers instead of plain pointers. Memory leaks are avoided and the object lifetimes are well defined because the objects are deleted when the smart pointers that own them go out of scope. In addition, using smart pointers is necessary to write exception-safe code. The constructors of classes that own more than one resource cannot release all of those resources when exceptions are thrown unless the resource management is moved to separate classes like smart pointers. [1]

There are many smart pointer implementations that are more suitable to certain tasks than auto_ptr but I will use auto_ptr here because it's the only smart pointer included in the C++ standard library. [2]

The following class uses auto_ptr to manage its only resource. Unfortunately though, when the class header is included by a user, the compiler will emit a diagnostic pointing to the fact that the destructor of the incomplete type CImpl is being called.

// c.h - The interface
#include <memory>
#include <string> 
class CImpl;// forward dcl

class C {
    std::auto_ptr<CImpl> p_;
    std::string s;
  private: // unimplemented
    C(C const &);
    C& operator=(C const &);
  public: // interface
    C(const std::string& s);
};

Note that in addition to the absence of the definitions of the copy constructor and the assignment operator, the destructor of the class is not declared. According to the C++ standard, if the destructor is not explicitly defined by the author, the compiler is required to define a destructor that calls the destructors of all of the data members. [3]

Assume that ctest.cpp, a user of class C, includes c.h. Here is what the compiler generated C destructor would look like in that translation unit:

// ctest.cpp: class C user
// Warning: pseudo code!
  inline C::~C() p_.~std::auto_ptr<CImpl>() 

When we replace the single line above with the inline definition of the auto_ptr destructor, the compiler generated destructor actually looks like this:

// p_ is variable defined
// in std::auto_ptr
inline C::~C() { delete p_; } 

Deleting the object is indeed what auto_ptr is responsible for. Now if we go one step further and decompose the delete statement, we arrive at the following code:

inline C::~C()
{
  // call destructor of
  // incomplete type
    p_->~CImpl();
  // some lib function
  // that releases memory
  __release__(p_);
} 

The problem is that the compiler generated destructor needs to call the destructor of an incomplete type. When faced with this situation, I used to include the header file of the incomplete class to make the compiler happy. Still, I was thinking how unfortunate it was that we couldn't replace plain pointers with auto_ptrs without increasing the compile time project file dependencies. [4]

This issue came up during a study group [5] where Allan questioned Herb Sutter's recommendation of using auto_ptr in the compiler firewall idiom. Besides the arguments about the suitability of auto_ptr in that idiom, there was another interesting question: how was Herb able to use an auto_ptr to an incomplete type?

The answer is too easy to justify ignoring it for years: Even though it is not necessary, declare the destructor of C, and provide an empty implementation in the class implementation file:

// c.cpp
#include "c.h"
#include <iostream>
using namespace std;
C::C(const string& s_) : s(s_), p_(new Cimpl(s_))
{
  cerr << "C::C() for " << s << endl;
}
// define empty dtor
C::~C() {} 

Now the compiler sees the declaration of the C destructor and does not generate it implicitly. As a result, the user code never directly calls the destructor of CImpl. As a user of CImpl, c.cpp already includes cimpl.h and has access to CImpl's definition. In fact, in the case of the compiler firewall idiom, c.cpp is the only intended user of cimpl.h.

Also note that defining the empty C destructor in the header file does not solve the problem. As Wayne put it nicely, it is where the destructor is defined that matters; it must be in the implementation file.

References:

[0] See Alan Griffiths' article Ending with the grin for more about this problem and solutions.

[1] Herb Sutter, "Exceptional C++," Addison Wesley, ISBN 0-201-61562-2

[2] See http://www.boost.org for other smart pointer implementations.

[3] ISO/IEC 14882, Programming Languages - C++, Section 12.4 Destructors

[4] See John Lakos' "Large Scale C++ Software Design" from Addison Wesley for issues around project file dependencies.

[5] The members of that meeting were Ali Çehreli, Allan Kelly, Walter Vannini, and Wayne Vucenic.