string_view
operator+
vs. StrCat()
absl::Status
std::bind
absl::optional
and std::unique_ptr
absl::StrFormat()
make_unique
and private
Constructors.bool
explicit
= delete
)switch
Statements Responsibly= delete
AbslHashValue
and Youcontains()
std::optional
parametersif
and switch
statements with initializersinline
Variablesstd::unique_ptr
Must Be MovedAbslStringify()
vector.at()
auto
for Variable DeclarationsOriginally 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?
// Safe (albeit a silly example): size_t len1 = strlen((s1 + s2).c_str()); size_t len2 = strlen(absl::StrCat(s1, s2).c_str());
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.
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!
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.