References, const, and overloading
A C++ reference is an alias to an existing object or function, not an owning handle and not
a nullable pointer by default. const on a reference means "this access path cannot modify
the referent," not "the object is frozen forever." Overloading then uses those types,
cv-qualifiers, and value categories to choose a function before runtime. This trio is why
modern C++ APIs can say "borrow mutably," "borrow read-only," or "consume this temporary"
without inventing a naming convention for every call.
The reset: references are about binding,
constis about what this expression may do, and overload resolution is compile-time dispatch over those facts.
How it really works
An lvalue reference T& must bind to an object you can modify through that reference. A
const T& can bind to const objects, non-const objects, and temporaries, but mutation
through that reference is not allowed. An rvalue reference T&& binds to expiring objects
and is the mechanism behind move semantics. The reference itself is not reseated after
binding; if you need optional or reseatable access, use a pointer or a wrapper such as
std::optional<std::reference_wrapper<T>>.
| Form | Meaning at the API boundary | Usual use |
|---|---|---|
T& |
required mutable borrow | fill, mutate, update in place |
const T& |
required read-only borrow | inspect without copying |
T* |
optional borrow or C boundary | nullable access, no ownership |
T&& |
expiring object may be consumed | move, sink, builder APIs |
const T* / const T& |
cannot modify through this path | read-only view of an object |
const composes with pointers and members in ways that matter. const int *p means p
points to an int you cannot modify through p. int *const p means p itself cannot be
reseated. A const member function promises not to modify the observable state of *this
through that member function. It can still read, call other const members, and mutate
fields marked mutable, which is why const is a type-system promise about an access path,
not a deep immutability proof.
Overload resolution starts with a set of candidate functions, filters for viable ones, and
ranks conversions. A mutable lvalue prefers T&. A const lvalue prefers const T&. A
temporary can bind to const T& or T&&, and T&& is the better match when present.
Member functions can also have cv-qualifiers and ref-qualifiers:
void append(std::string_view line) &;
void append(std::string_view line) && = delete;
That says append may be called on an lvalue object, but not on a temporary. This is
ordinary overload resolution with the implicit object parameter included in the decision.
Executable artifact: overload selection is visible
The runnable demo lives in
examples/modern-cpp/references-const-and-overloading/. It mutates through Packet&,
inspects through const Packet&, and selects different overloads for mutable lvalues,
const lvalues, and temporaries.
cd examples/modern-cpp/references-const-and-overloading
./run.sh
The central overload set is small enough to read as a table:
static void describe(Packet &) {
std::cout << "overload: mutable lvalue\n";
}
static void describe(const Packet &) {
std::cout << "overload: const lvalue\n";
}
static void describe(Packet &&) {
std::cout << "overload: rvalue, can be consumed\n";
}
The calls choose without runtime branching:
Packet packet{"frame", 64};
const Packet frozen{"frozen", 128};
describe(packet);
describe(frozen);
describe(Packet{"temporary", 8});
The demo also uses a ref-qualified member:
void append(std::string_view line) & {
lines_.push_back(std::string(line));
}
void append(std::string_view) && = delete;
That prevents accidental mutation of a short-lived Log{} temporary. The type communicates
"this operation needs a stable object," and the compiler enforces it at the call site.
Failure modes & trade-offs
- Dangling references are still UB. Returning
T&to a local, storing a reference to a temporary beyond its lifetime, or keeping a reference into a reallocated container is as broken as keeping a dangling pointer. constis not ownership and not deep immutability. Another alias can mutate the same object, andmutablefields can change insideconstmember functions.- Overloads can become ambiguous. Too many overloads, implicit conversions, and default arguments can make a call hard for humans or the compiler to choose.
const_castis not a back door to safety. Removing const and modifying an actually const object is undefined behavior.- Reference data members are sticky. They must be initialized, cannot be reseated, and can make assignment operators deleted or surprising. Prefer pointers for optional relationship fields.
- Rvalue-reference parameters are lvalues when named. Inside
void f(T&& x), the expressionxis an lvalue. Usestd::move(x)only when you intentionally consume it.
In practice
- Use references for required borrows.
T&andconst T&say the caller must provide a real object and ownership stays outside. - Use pointers when null matters. A nullable parameter should look nullable. Do not use a reference plus out-of-band state.
- Prefer
constearly. Mark inspector functions and read-only parametersconst; it expands what can call them and documents non-mutation. - Keep overload sets semantic. Overloads should represent the same operation over different types or value categories, not unrelated commands with one name.
- Use ref-qualifiers for fluent APIs. They can prevent calls that would mutate or return references from temporaries.
Connects to: Move semantics and value categories · Smart pointers: unique_ptr, shared_ptr, ownership · Const correctness and what const really promises · What a pointer really is · The C type system is weak · Integer promotions and implicit conversions
Sources
- cppreference - Reference declaration - lvalue references, rvalue references, reference collapsing, and dangling-reference notes. https://en.cppreference.com/w/cpp/language/reference
- cppreference - cv qualifiers -
constandvolatilequalification, const objects, and cv-qualified member functions. https://en.cppreference.com/w/cpp/language/cv - cppreference - Overload resolution - candidate selection, viability, conversion ranking, and best viable function rules. https://en.cppreference.com/w/cpp/language/overload_resolution
- C++ working draft -
[dcl.ref]- standard wording for reference declarators and reference rules. https://eel.is/c++draft/dcl.ref - C++ working draft -
[over.match]- standard wording for overload resolution and matching. https://eel.is/c++draft/over.match - C++ Core Guidelines - practical guidance for passing by reference, const-correctness, and overload clarity. https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines