Will an 'empty' constructor or destructor do the same thing as the generated one?

34,236

Solution 1

It will do the same thing (nothing, in essence). But it's not the same as if you didn't write it. Because writing the destructor will require a working base-class destructor. If the base class destructor is private or if there is any other reason it can't be invoked, then your program is faulty. Consider this

struct A { private: ~A(); };
struct B : A { }; 

That is OK, as long as your don't require to destruct an object of type B (and thus, implicitly of type A) - like if you never call delete on a dynamically created object, or you never create an object of it in the first place. If you do, then the compiler will display an appropriate diagnostic. Now if you provide one explicitly

struct A { private: ~A(); };
struct B : A { ~B() { /* ... */ } }; 

That one will try to implicitly call the destructor of the base-class, and will cause a diagnostic already at definition time of ~B.

There is another difference that centers around the definition of the destructor and implicit calls to member destructors. Consider this smart pointer member

struct C;
struct A {
    auto_ptr<C> a;
    A();
};

Let's assume the object of type C is created in the definition of A's constructor in the .cpp file, which also contains the definition of struct C. Now, if you use struct A, and require destruction of an A object, the compiler will provide an implicit definition of the destructor, just like in the case above. That destructor will also implicitly call the destructor of the auto_ptr object. And that will delete the pointer it holds, that points to the C object - without knowing the definition of C! That appeared in the .cpp file where struct A's constructor is defined.

This actually is a common problem in implementing the pimpl idiom. The solution here is to add a destructor and provide an empty definition of it in the .cpp file, where the struct C is defined. At the time it invokes the destructor of its member, it will then know the definition of struct C, and can correctly call its destructor.

struct C;
struct A {
    auto_ptr<C> a;
    A();
    ~A(); // defined as ~A() { } in .cpp file, too
};

Note that boost::shared_ptr does not have that problem: It instead requires a complete type when its constructor is invoked in certain ways.

Another point where it makes a difference in current C++ is when you want to use memset and friends on such an object that has a user declared destructor. Such types are not PODs anymore (plain old data), and these are not allowed to be bit-copied. Note that this restriction isn't really needed - and the next C++ version has improved the situation on this, so that it allows you to still bit-copy such types, as long as other more important changes are not made.


Since you asked for constructors: Well, for these much the same things are true. Note that constructors also contain implicit calls to destructors. On things like auto_ptr, these calls (even if not actually done at runtime - the pure possibility already matters here) will do the same harm as for destructors, and happen when something in the constructor throws - the compiler is then required to call the destructor of the members. This answer makes some use of implicit definition of default constructors.

Also, the same is true for visibility and PODness that i said about the destructor above.

There is one important difference regarding initialization. If you put a user declared constructor, your type does not receive value initialization of members anymore, and it is up to your constructor to do any initialization that's needed. Example:

struct A {
    int a;
};

struct B {
    int b;
    B() { }
};

In this case, the following is always true

assert(A().a == 0);

While the following is undefined behavior, because b was never initialized (your constructor omitted that). The value may be zero, but may aswell be any other weird value. Trying to read from such an uninitialized object causes undefined behavior.

assert(B().b == 0);

This is also true for using this syntax in new, like new A() (note the parentheses at the end - if they are omitted value initialization is not done, and since there is no user declared constructor that could initialize it, a will be left uninitialized).

Solution 2

I know I'm late in the discussion, nevertheless my experience says that the compiler behaves differently when facing an empty destructor compared to a compiler generated one. At least this is the case with MSVC++ 8.0 (2005) and MSVC++ 9.0 (2008).

When looking at the generated assembly for some code making use of expression templates, I realized that in release mode, the call to my BinaryVectorExpression operator + (const Vector& lhs, const Vector& rhs) was never inlined. (please don't pay attention to the exact types and operator signature).

To further diagnose the problem, I enabled the various Compiler Warnings That Are Off by Default. The C4714 warning is particularly interesting. It is emitted by the compiler when a function marked with __forceinline doesn't get inlined nonetheless.

I enabled the C4714 warning and I marked the operator with __forceinline and I could verify the compiler reports it was unable to inline the call to the operator.

Among the reasons described in the documentation, the compiler fails to inline a function marked with __forceinline for:

Functions returning an unwindable object by value when -GX/EHs/EHa is on

This is the case of my BinaryVectorExpression operator + (const Vector& lhs, const Vector& rhs). BinaryVectorExpression is returned by value and even though its destructor is empty, it makes this return value being considered as an unwindable object. Adding throw () to the destructor didn't help the compiler and I avoid using exception specifications anyway. Commenting out the empty destructor let the compiler fully inline the code.

The take-away is that from now, in every class, I write commented out empty destructors to let humans know the destructor does nothing on purpose, the very same way people comment out the empty exception specification `/* throw() */ to indicate that the destructor cannot throw.

//~Foo() /* throw() */ {}

Hope that helps.

Solution 3

The empty destructor that you defined out of class has similar semantics in most regards, but not in all.

Specifically, the implicitly defined destructor
1) is an inline public member (yours is not inline)
2) is denoted as a trivial destructor (necessary to make trivial types that can be in unions, yours cannot)
3) has an exception specification (throw(), yours does not)

Solution 4

Yes, that empty destructor is the same as the automatically-generated one. I've always just let the compiler generate them automatically; I don't think it's necessary to specify the destructor explicitly unless you need to do something unusual: make it virtual or private, say.

Solution 5

I agree with David except that I would say it is generally a good practice to define a virtual destructor i.e.

virtual ~Foo() { }

missing out virtual destructor can lead to memory leak because people who inherit from your Foo class may not have noticed that their destructor will never be called!!

Share:
34,236
Andrew Song
Author by

Andrew Song

Computer Science Student at the University of Waterloo. Former internships at: Research in Motion, doing Integration Testing and Radio Layer Software Development Fog Creek Software, working on Kiln Facebook, working on the Site Integrity team Google, working on the Google Travel team You can also find out more about me via my StackOverflow CV

Updated on July 05, 2022

Comments

  • Andrew Song
    Andrew Song almost 2 years

    Suppose we have a (toy) C++ class such as the following:

    class Foo {
        public:
            Foo();
        private:
            int t;
    };
    

    Since no destructor is defined, a C++ compiler should create one automatically for class Foo. If the destructor does not need to clean up any dynamically allocated memory (that is, we could reasonably rely on the destructor the compiler gives us), will defining an empty destructor, ie.

    Foo::~Foo() { }
    

    do the same thing as the compiler-generated one? What about an empty constructor -- that is, Foo::Foo() { }?

    If there are differences, where do they exist? If not, is one method preferred over the other?

  • Tom Leys
    Tom Leys almost 15 years
    +1 for the mention of forward declared automatic pointers and the automatic destructor. A common gotcha when you start forward declaring stuff.
  • James Hopkin
    James Hopkin almost 15 years
    Your first example is a little strange. The B you've written can't be used at all (new-ing one would be an error, any cast to one would be undefined behaviour, since it's non-POD).
  • James Hopkin
    James Hopkin almost 15 years
    Also, A().a == 0 is only true for statics. A local variable of type A will be uninitialised.
  • Johannes Schaub - litb
    Johannes Schaub - litb almost 15 years
    @James, you could still new a B - as long as you don't delete it :) A() will value-initialize its member if there is no user declared constructor. That means that a will be zero. It's different from { A a; } which wouldn't initialize it. { A a(); } would, if it weren't a function =)
  • Mathias
    Mathias almost 15 years
    @litb: should your first line in the construtor section read: "Note that constructors also contain implicit calls to constructors" instead of destructors?
  • Johannes Schaub - litb
    Johannes Schaub - litb almost 15 years
    eJames, they certainly contain calls to constructors, too. But they may also contain calls to destructors. consider: struct A { A() { throw 42; } }; struct B { string s; A a; B() { } }; << this constructor B contains an implicit call to ~string, and in case of when A::A throws, it will actually call it. This will cause instantiation of ~string (if it were a smart pointer class template). Some smart pointers check in their dtor for completeness of the type they delete on, and may cause compile time errors at this point.
  • palantus
    palantus almost 15 years
    That should be assert(B().b == 0) near the end, shouldn't it?
  • Johannes Schaub - litb
    Johannes Schaub - litb almost 15 years
    @ericholtman, please read what i wrote in the comment above. Initializing using T() is a common applied technique, especially in template programming. "C++ does not value initialize PODs (int, double) in default constructors" => i did not say such a thing. Calling the default constructor is different from default-initializing or value initializing for POD structs. Read 5.2.3/2, 8.5/5, 8.4/9 and 12.6.2, if you want to know further. Also boost.org/doc/libs/1_39_0/libs/utility/value_init.htm would be a worthwhile read. Have fun
  • dalle
    dalle almost 15 years
    A note on 3: The exception specification isn't always empty in an implicitly defined destructor, as noted in [except.spec].
  • Faisal Vali
    Faisal Vali almost 15 years
    @dalle +1 on comment - thanks for drawing attention to that - you are indeed correct, if Foo had derived from base classes each with non-implicit destructors with exception specifications - Foo's implicit dtor would have "inherited" the union of those exception specifications - in this case, since there is no inheritance, the implicit dtor's exception specification happens to be throw().
  • Keyur Padalia
    Keyur Padalia almost 14 years
    @litb: I'm in a situation similar to your example with auto_ptr. I would expect a compilation error here, because the template expansion of auto_ptr<C> will result in a call to C::~C(). What I'm seeing instead is that the C object silently fails to be destructed. How is this possible? Does auto_ptr do some weird void* casting magic? What am I missing?
  • Johannes Schaub - litb
    Johannes Schaub - litb almost 14 years
    @Thomas the Standard allows IncompleteType *c = ...; delete c;. In case the type would have an empty destructor this will be well defined. In case it will have a non-empty destructor, it will cause undefined behavior. What you probably see is undefined behavior -- the compiler assumes an empty destructor and doesn't call it.
  • Keyur Padalia
    Keyur Padalia almost 14 years
    Yikes! That is a weird thing for the standard to allow, IMHO. Thanks for clearing that up.
  • Keyur Padalia
    Keyur Padalia almost 14 years
    @litb: Sorry for picking your brain about this one, but I can't seem to find a good online reference about this issue. I tried to create a minimal example, but failed. Maybe g++ 4.4.3 only assumes this empty destructor in particular situations... Maybe you can do better? Much obliged!