GotW #22

Home Blog Talks Books & Articles Training & Consulting

On the
blog
RSS feed November 4: Other Concurrency Sessions at PDC
November 3
: PDC'09: Tutorial & Panel
October 26: Hoare on Testing
October 23
: Deprecating export Considered for ISO C++0x

This is the original GotW problem and solution substantially as posted to Usenet. See the book Exceptional C++ (Addison-Wesley, 2000) for the most current solutions to GotW issues #1-30. The solutions in the book have been revised and expanded since their initial appearance in GotW. The book versions also incorporate corrections, new material, and conformance to the final ANSI/ISO C++ standard.

Object Lifetimes - Part I
Difficulty: 5 / 10

"To be, or not to be..." When does an object actually exist? This problem considers when an object is safe to use.

Problem

Critique the following code fragment. Is the code in #2 safe and/or legal? Explain.

    void f() {
      T t(1);
      T& rt = t;
      // #1: do something with t or rt
      t.~T();
      new (&t) T(2);
      // #2: do something with t or rt
    } // t is destroyed again

Solution

Yes, #2 is safe and legal (if you get to it), but:

a) the function as a whole is not safe; and

b) it's a bad habit to get into.

Why #2 Is Safe (If You Get To It)

The draft explicitly allows this code. The reference rt is not invalidated by the in-place destruction and reconstruction. (Of course, you can't use t or rt between the t.~T() and the placement new, since during that time no object exists. We're also assuming that T::operator&() hasn't been overloaded to do something other than return the object's address.)

The reason we say #2 is safe "if you get to it" is that f() as a whole may not be exception-safe:

Why the Function Is Not Safe

If T's constructor may throw in the T(2) call, then f() is not exception-safe. Consider why: If the T(2) call throws, then no new object has been reconstructed in the 't' memory area, yet at the end of the function T::~T() is naturally called (since t is an automatic variable) and "t is destroyed again" like the comment says. That is, 't' will be constructed once but destroyed twice (oops). This is likely to create unpredictable side effects, such as core dumps.

Why This Is a Bad Habit

Ignoring the exception safety issues, the code happens to work in this setting because the programmer knows the complete type of the object being constructed and destroyed. That is, it was a T and is being destroyed and reconstructed as a T.

This technique is rarely if ever necessary in real code, and is a very bad habit to get into because it's fraught with (sometimes subtle) dangers if it appears in a member function:

    void T::f( int i ) {
      this->~T();
      new (this) T(i);
    }

Now is this technique safe? In general, No. Consider the following code:

    class U : /*...*/ public T { /* ... */ };

    void f() {
      /*AAA*/ t(1);
      /*BBB*/& rt = t;
      // #1: do something with t or rt
      t.f(2);
      // #2: do something with t or rt
    } // t is destroyed again

If "/*AAA*/" is "T", the code in #2 will still work, even if "/*BBB*/" is not "T" (it could be a base class of T).

If "/*AAA*/" is "U", all bets are off no matter what "/*BBB*/" is. Probably the best you can hope for is an immediate core dump, because the call t.f() "slices" the object. Here, the slicing issue is that t.f() replaces the original object with another object of a different type -- that is, a T instead of a U. Even if you're willing to write nonportable code, there's no way of knowing whether the object layout of a T is even usable as a U when superimposed in memory where a U used to be. Chances are good that it's not. Don't go there... this is never a good practice.

This GotW has covered some basic safety and slicing issues of in-place destruction and reconstruction. This sets the stage for the followup question in GotW #23: Object Lifetimes - Part II.

Copyright © 2009 Herb Sutter