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()
Originally posted as TotW #116 on May 26, 2016
Updated 2020-06-01
Quicklink: abseil.io/tips/116
From painting to image, from image to text, from text to voice, a sort of imaginary pointer indicates, shows, fixes, locates, imposes a system of references, tries to stabilize a unique space. — This is Not a Pipe by Michel Foucault
Used as arguments of functions, const references have several advantages when compared to pointers to const: they cannot be null and it’s quite clear that the function is not taking ownership of the object. But they have other differences that can sometimes be problematic: they are more implicit (i.e. there is nothing on the call site showing we are taking a reference) and they can be bound to a temporary.
Consider the following class as an example:
class Foo { public: explicit Foo(const std::string& content) : content_(content) {} const std::string& content() const { return content_; } private: const std::string& content_; };
It looks reasonable. But what happens if we build a Foo
from a string literal?
void Func() { Foo foo("something"); LOG(INFO) << foo.content(); // BOOM! }
When creating foo
, the content_
member gets bound to the temporary
std::string
object that was created from the literal and passed to the
constructor. The temporary string goes out of scope at the end of the line where
it was created. Now foo.content_
is a reference to an object that no longer
exists. Accessing it is undefined behavior and anything can happen, from working
fine in tests to going really wrong in production.
In our example, the simplest solution is probably to pass and store the string by value. But let’s assume that we need to refer to the original argument, e.g. because it’s not a string, but some more interesting type. The solution is to pass the argument by pointer:
class Foo { public: // Do not forget this comment: // Does not take ownership of content, which must refer to a valid string that // outlives this object. explicit Foo(const std::string* content) : content_(content) {} const std::string& content() const { return *content_; } private: const std::string* const content_; // not owned, can't be null };
Now the following will simply fail to compile:
std::string GetString(); void Func() { Foo foo1(&GetString()); // error: taking the address of a temporary of // type 'std::string' Foo foo2(&"something"); // error: no matching constructor for initialization // of 'Foo' }
And it will be pretty clear at call site that the object might keep the address of the argument:
void Func2() { std::string content = GetString(); Foo foo(&content); }
You might have noticed that we say twice that the pointer cannot be null and that it is not owned, once in the documentation of the constructor then in the comment for the instance variable. Is this necessary? Consider this:
class Baz { public: // Does not take any ownership, and all pointers must refer to valid objects // that outlive the one constructed. explicit Baz(const Arg1* arg1, Arg2* arg2) : arg1_(*arg1), arg2_(*arg2) {} private: // It is now clear that we do not have ownership and that the references can't // be null. const Arg1& arg1_; Arg2& arg2_; // Yes, non-const references are style-compliant! };
One downside of members of reference type is that you cannot reassign them, meaning that your class will not have a copy assignment operator (copy constructors are still OK) but it might make sense to explicitly delete it to respect the rule of 3. If your class should be assignable you’ll need non-const pointers, still potentially to const objects. Tip #177 discusses this in more detail.
If you want defense in depth and think some caller might accidentally pass a
null pointer, you can use *ABSL_DIE_IF_NULL(arg1)
to cause a crash. Note that
just dereferencing the null pointer is not, as is commonly believed, guaranteed
to crash; rather it is undefined behavior and should not be relied upon. Here
what would probably happen is that since a reference is implemented as a
pointer, it will just be copied and the crash will happen later, when someone
actually accesses the field.
It is still OK to pass an argument by const reference to a constructor if the
argument is copied, or just used in the constructor and no reference to it is
kept in the constructed object. In other cases, consider passing arguments by
pointers (to const or not). Also remember that if you are actually transferring
the ownership of an object, it should be passed as a std::unique_ptr
.
Lastly, what is discussed here is not limited to constructors: any function that somehow keeps an alias to one of its arguments, whether by putting a pointer in a cache or binding the argument in a detached function, should take that argument by pointer.