Tip of the Week #5: Disappearing Act

Originally posted as TotW #5 on June 26, 2012

Updated 2020-06-01

Quicklink: abseil.io/tips/5

“Don’t know what you got till it’s gone.” –Cinderella

Sometimes, in order to use a C++ library the right way, you need to understand both the library and the language. So … what’s wrong with the following?

// DON’T DO THIS
std::string s1, s2;
...
const char* p1 = (s1 + s2).c_str();             // Avoid!
const char* p2 = absl::StrCat(s1, s2).c_str();  // Avoid!

Both (s1+s2) and absl::StrCat(s1,s2) create temporary objects (in both cases, strings, but the same rules apply for any objects). The member function c_str() returns a pointer to data that lives as long as the temporary object. How long does the temporary object live? According to the C++17 standard [class.temporary], “Temporary objects are destroyed as the last step in evaluating the full-expression that (lexically) contains the point where they were created.” (A “full-expression” is “an expression that is not a subexpression of another expression”). In each example above, as soon as the expression on the right side of the assignment operator was completed, the temporary value was destroyed, and the return value from c_str() became a dangling pointer. tl;dr? By the time you hit a semicolon (often sooner), the temporary object is history. Yikes! How can you avoid this kind of problem?

Option 1: Finish Using the Temporary Object Before the End of the full-expression:

// Safe (albeit a silly example):
size_t len1 = strlen((s1 + s2).c_str());
size_t len2 = strlen(absl::StrCat(s1, s2).c_str());

Option 2: Store the Temporary Object.

You’re creating an object (on the stack) anyway; why not hang on to it for a while? This is cheaper than it might first appear. Because of something called “return value optimization,” (and move semantics on many value types, see Tip #77) the temporary object will be constructed in the variable you’re “assigning” it to, and won’t be copied:

// Safe (and more efficient than you might think):
std::string tmp_1 = s1 + s2;
std::string tmp_2 = absl::StrCat(s1, s2);
// tmp_1.c_str() and tmp_2.c_str() are safe.

Option 3: Store a Reference to the Temporary Object.

C++17 standard [class.temporary]: “The temporary to which the reference is bound or the temporary that is the complete object of a sub-object to which the reference is bound persists for the lifetime of the reference.”

Because of return value optimization, this usually isn’t any cheaper than storing the object itself (Option 2), and it is potentially confusing or worrisome (see Tip #101). (Exceptional cases that need to use lifetime extension should be commented!)

// Equally safe:
const std::string& tmp_1 = s1 + s2;
const std::string& tmp_2 = absl::StrCat(s1, s2);
// tmp_1.c_str() and tmp_2.c_str() are safe.
// The following behavior is dangerously subtle:
// If the compiler can see you’re storing a reference to a
// temporary object’s internals, it will keep the whole
// temporary object alive.
// struct Person { string name; ... }
// GeneratePerson() returns an object; GeneratePerson().name
// is clearly a sub-object:
const std::string& person_name = GeneratePerson().name; // safe
// If the compiler can’t tell, you’re at risk.
// class DiceSeries_DiceRoll { `const string&` nickname() ... }
// GenerateDiceRoll() returns an object; the compiler can’t tell
// if GenerateDiceRoll().nickname() is a subobject.
// The following may store a dangling reference:
const std::string& nickname = GenerateDiceRoll().nickname(); // BAD!

Option 4: Design your functions so they don’t return objects???

Many functions follow this principle; but many don’t. Sometimes it really is better to return an object than to require the caller to pass in a pointer to an output parameter. Be aware of when the creation of temporaries can happen. Anything that returns a pointer or reference to an object’s internals is potentially a problem when operating on a temporary object. c_str() is the most obvious culprit, but protobuf getters (mutable or otherwise) and getters in general can be equally problematic.


Subscribe to the Abseil Blog