References and Pointers Recap

This chapter was about the two ways C++ lets one piece of code reach an object that lives somewhere else: references, which are aliases, and pointers, which are objects holding an address. Most of the chapter's rules exist to answer one question, which is whether the thing you are reaching for is still alive.

Compound Types and Value Categories

Compound data types, also called composite data types, are types built out of fundamental types or other compound types. References and pointers are both compound types, as are arrays, structs, and classes.

The value category of an expression describes what the expression resolves to. An lvalue evaluates to a function or an object with an identity, meaning it has an identifier or an address you could take. Lvalues split into modifiable ones and non-modifiable ones, the latter usually because they are const or constexpr. An rvalue is any expression that is not an lvalue: literals other than string literals, and values returned by value from functions and operators.

The distinction matters because it determines what a reference is allowed to bind to.

References

A reference is an alias for an object that already exists. Every operation on the reference is an operation on the object it refers to. When a reference is initialized with an object it is bound to it, and that object is the referent.

Two rules follow, and both are absolute:

  • A reference cannot be reseated. Once bound, it refers to that object for its whole life.
  • A plain lvalue reference cannot bind to a non-modifiable lvalue or to an rvalue, since that would provide a route to modify something that is not modifiable. This is why plain lvalue references are sometimes called lvalue references to non-const.

Adding const changes the second rule. A reference to const may bind to a modifiable lvalue, a non-modifiable lvalue, or an rvalue. Binding one to an rvalue creates a temporary object, an unnamed object created and destroyed within a single expression, and the reference extends its life for as long as the reference exists.

If the referent is destroyed while a reference to it is still around, the result is a dangling reference, and using it is undefined behavior.

Pointers

The address-of operator & yields the address of its operand, and the dereference operator * yields the object at an address as an lvalue. A pointer is an object that stores such an address.

Pointers are not initialized for you, and three states are worth naming:

State Meaning Safe to dereference
Wild Never initialized, holds whatever was in memory No
Dangling Held a valid address, but the object is gone No
Null Holds nullptr, deliberately points at nothing No, but testable

That table is the argument for the chapter's central pointer rule: keep every pointer either aimed at a live object or set to nullptr, never anything else. Follow it and a single null check is enough, because any non-null pointer is known to be good.

const can apply to either half of a pointer. A pointer to const points at a value it may not change through the pointer, though the pointer itself can be repointed. A const pointer may not be repointed, though the value can be changed through it. A const pointer to a const value allows neither.

Choosing How to Pass an Object

Method Parameter form Copies the argument Can modify the caller's object
Pass by value T param Yes No
Pass by reference T& param No Yes
Pass by const reference const T& param No No
Pass by address T* param Copies the pointer only Yes, through the pointer

Pass by reference binds the parameter to the caller's object, so nothing is copied and the function operates on the original. Pass by const reference gives the same efficiency while promising not to modify, which makes it the default for anything expensive to copy. Pass by address copies a pointer rather than an object and reaches the original by dereferencing it, which means the function has to consider whether the pointer might be null.

This is also where in parameters, out parameters, and in-out parameters come in: describing which direction data flows through each parameter is what tells you which form to use.

Returning References and Addresses

Return by reference hands back a reference bound to the returned object rather than a copy. It carries one serious caveat: the object must outlive the function. Returning a reference to a local variable leaves the caller with a dangling reference and undefined behavior. Returning a parameter that was itself passed in by reference is safe, because it was already alive before the call.

Return by address is the same idea with a pointer instead of a reference, and carries the same caveat.

Be aware that assigning the result of a function that returns a reference to a non-reference variable copies the object, exactly as if it had been returned by value.

This mistake is common enough that the compiler looks for it:

#include <iostream>

int* createValue()
{
    int result{ 42 };

    return &result;
}

int main()
{
    int* ptr{ createValue() };

    std::cout << *ptr << '\n';

    return 0;
}

This code is wrong, and the compiler says so:

bad.cpp: In function 'int* createValue()':
bad.cpp:7:12: warning: address of local variable 'result' returned [-Wreturn-local-addr]
    7 |     return &result;
      |            ^~~~~~~
bad.cpp:5:9: note: declared here
    5 |     int result{ 42 };
      |         ^~~~~~

result is destroyed when createValue returns, so the caller receives the address of memory that is no longer an object.

Type Deduction

When auto deduces a variable's type it drops references and top-level const. Low-level const, the kind that applies to what a pointer or reference refers to, is kept. If you want a reference or a top-level const, reapply it in the declaration, as in const auto&.

Representing a Missing Value

std::optional holds either a value or nothing, which gives a function a way to report "no result" without inventing a sentinel value or returning a pointer that the caller must remember to null check.

Terms Used in This Chapter

  • Compound data type: a type built from fundamental types or other compound types
  • Value category: what an expression resolves to
  • lvalue: an expression evaluating to an object with an identity, either modifiable or non-modifiable
  • rvalue: any expression that is not an lvalue
  • Identity: having an identifier or an address that can be taken
  • Reference: an alias for an existing object
  • Bound and referent: the act of attaching a reference to an object, and the object attached
  • Reseat: rebinding a reference to a different object, which C++ does not permit
  • Reference to const: a reference that treats its referent as const and may bind to rvalues
  • Temporary object: an unnamed object created and destroyed inside one expression
  • Dangling reference: a reference whose referent has been destroyed
  • Address-of operator & and dereference operator *: obtain an address, and access the object at one
  • Pointer: an object that stores an address
  • Wild pointer: an uninitialized pointer
  • Dangling pointer: a pointer to an object that no longer exists
  • Null pointer and nullptr: a pointer that deliberately points at nothing, and the literal for it
  • Pointer to const and const pointer: constness applied to the pointed-to value, or to the pointer itself
  • Pass by reference and pass by address: giving a function access via an alias, or via an address
  • Return by reference and return by address: returning an alias or an address instead of a copy

Looking Forward

The habit to carry forward is asking who owns an object and how long it lives before writing anything that refers to it. Every dangling reference and dangling pointer is that question going unanswered. Next you will use these tools on objects whose lifetime you control directly through dynamic allocation, and then on smart pointers, which encode ownership in the type so the answer is written down rather than remembered.