Tip of the Week #116: Keeping References on Arguments

Originally posted as TotW #116 on May 26, 2016

By Alex Pilkiewicz

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

Const References vs. Pointers to Const

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.

Risk of a Dangling Reference in Classes

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.

A Solution: Use Pointers

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);
}

One Step Further, One Less Comment: Storing a Reference

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.

Conclusion

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.


Subscribe to the Abseil Blog