views:

1924

answers:

7

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?

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

EDIT: Is the answer the same for constructors as well?

+4  A: 

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.

David Seiler
+4  A: 

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)

Faisal Vali
A note on 3: The exception specification isn't always empty in an implicitly defined destructor, as noted in [except.spec].
dalle
@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().
Faisal Vali
A: 

I'd say best to put the empty declaration, it tells any future maintainers that it wasn't an oversight, and you really did mean to use the default one.

Ape-inago
+1  A: 

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!!

oykuo
+43  A: 
Johannes Schaub - litb
+1 nice analysis!
Faisal Vali
Wow, quite detailed. Thank you for the thorough explanation!
Andrew Song
you're welcome :)
Johannes Schaub - litb
+1 for the mention of forward declared automatic pointers and the automatic destructor. A common gotcha when you start forward declaring stuff.
Tom Leys
litb, are you a robot? How on earth do you generate such high-quality posts every time?
rlbond
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
Also, A().a == 0 is only true for statics. A local variable of type A will be uninitialised.
James Hopkin
@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 =)
Johannes Schaub - litb
Anyway, i'm glad you people like this :)
Johannes Schaub - litb
@litb: should your first line in the construtor section read: "Note that constructors also contain implicit calls to _constructors_" instead of _destructors_?
e.James
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.
Johannes Schaub - litb
That should be assert(B().b == 0) near the end, shouldn't it?
Michael Myers
@mmyers, indeed. thanks
Johannes Schaub - litb
"In this case, the following is always trueassert(A().a == 0);"That's incorrect.Unless you explicitly write a constructor for A::A(), and initialize member a, its value will be undefined.C++ does *not* value initialize PODs (int, double) in default constructors, nor will it do in an constructor your write, unless you explicitly do it yourself.
Eric H.
@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 http://www.boost.org/doc/libs/1_39_0/libs/utility/value_init.htm would be a worthwhile read. Have fun
Johannes Schaub - litb
@litb - great answer, although the fine print of value initialization is (or was until recently) difficult to rely on due to uneven implementation by many widely used compilers. So @Eric H. - you may be better off with your current assumptions for now!
Daniel Earwicker
@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?
Thomas
@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.
Johannes Schaub - litb
Yikes! That is a weird thing for the standard to allow, IMHO. Thanks for clearing that up.
Thomas
@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!
Thomas
+1  A: 
+2  A: 

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.

Gregory Pakosz
+1 Nice research!
Ofek Shilon