[ Team LiB ] Previous Section Next Section

Gotcha #43: Temporary Lifetime

In certain circumstances, the compiler is forced to create temporary objects. The standard states that the lifetime of such a temporary is from its point of creation to the end of the largest enclosing expression (what the standard calls the "full-expression"). A common problem is unintended dependence on the continued existence of these temporaries after they've been destroyed:



class String { 


 public:


   // . . .


   ~String()


       { delete [] s_; }


   friend String operator +( const String &, const String & );


   operator const char *() const


       { return s_; }


 private:


   char *s_;


};


// . . .


String s1, s2;


printf( "%s", (const char *)(s1+s2) ); // #1


const char* p = s1+s2; // #2


printf( "%s", p ); // #3


The implementation of String's binary + operator often requires that the return value be stored in a temporary. This is the case for both its uses above. In the instance marked #1 above, the result of s1+s2 is dumped into a temporary, which is then converted to a const char * prior to being passed to printf. After the call to printf returns, the temporary String object is destroyed. This works because the temporary has lived as long as any of its uses.

In the instance marked #2, the result of s1+s2 is dumped into a temporary, which is then converted to a const char * as before. The difference in this case is that the String temporary is destroyed at the end of the initialization of the pointer p. When p is used in the call to printf, it's referring to a buffer held by a destroyed String object. Undefined behavior.

The truly unfortunate aspect of this particular bug is that the code may well continue to work (as least during testing). For example, when the String temporary deletes its character array buffer, the array delete operator may simply mark the storage as unused, without changing its content. If the storage is not reused between lines #2 and #3, the code will appear to work. If this piece of code is later embedded in a multithreaded application, it will fail sporadically.

It's better to either employ a complex expression or declare an explicit temporary with an extended lifetime:



String temp = s1+s2; 


const char *p = temp;


printf( "%s", p );


Note, however, that the limited lifetime of temporaries can often be used to advantage. One common practice when programming with the standard library is to customize components with function objects:



class StrLenLess 


   : public binary_function<const char *, const char *, bool> {


 public:


   bool operator() ( const char *a, const char *b ) const


       { return strlen(a) < strlen(b); }


};


// . . .


sort( start, end, StrLenLess() );


The expression StrLenLess() causes the compiler to generate an anonymous temporary object that exists until the return from the sort algorithm. The alternative of using an explicitly named variable is longer and pollutes the current scope with a useless name (see Gotcha #48):



StrLenLess comp; 


sort( start, end, comp );


// comp is still in scope . . .


Another complication with temporary lifetimes can occur with legacy code written to a pre-standard C++ compiler. Prior to publication of the standard, there was no universal rule for temporary lifetime. As a result, some compilers would destroy temporaries at the end of the block in which they came into existence, some would destroy them at the end of the statement in which they came into existence, and so on. When refactoring legacy code, be alert for any silent changes of meaning due to changes in temporary lifetime.

    [ Team LiB ] Previous Section Next Section