Conferences are a great source of inspiration, partly trought talks and partly trough the best track, the hallway track. I already mentioned that at CppCon one of my favourite talks was the one by Steve Downey on std::optional<T&>. He also mentioned that there are three different uses of references in C++. I was thinking whether it’s four and disucssed this the next day during the break between two talks.
Let’s talk about the 3 usages and I’ll give you the extra one which is not a usage of references but what I was thinking about originally.
Calling Conventions
One of the most common uses of references are function parameters. We often pass arguments by reference to avoid unnecessary copies — especially when dealing with non-POD types. By default we take parameters by const T& to ensure that the function cannot modify the argument whose copy we avoided with the help of using a reference.
If we want to allow the function to modify its parameters, we should use a plain reference.
When an argument is passed by reference, we typiically also express that a value must be present - unlike for a pointer - and that we don’t pass ownership.
Local Aliases
Another everyday use of references is creating local aliases. By declaring a variable as T& or auto& possibly with a const added, you can refer to an existing object without copying it. Just like when you do the same for a function parameter.
You see this pattern all the time in range-based for loops:
1
2
3
for (auto& item : container) {
item.doSomething();
}
Here, item is just an alias to each element of container. Without the &, every iteration would copy the element, which could be both wasteful and slow.
Local references are also handy for readability. Instead of repeatedly typing a long expression, you can give it a short, meaningful name:
1
2
auto& settings = config.user.profile.settings;
settings.enableFeatureX();
This makes it clear that you’re modifying the original settings, not a copy.
Usig local aliases is, of course, not a silver bullet. Use them with moderation and you’ll make your code more readable, but if you overuse them, they can make code harder to follow make you jumping back and forth in a function to figure out the meaning of aliases.
References as Members
The third usage Steve referred to was the reference qualification of member variables.
1
2
3
struct Foo {
int& x;
};
This often feels like a good idea - storing only a reference in an object instead of making a copy. And sometimes it is. But we often forget it also changes some of the fundamental characteristics of your type in subtle ways.
First of all, Foo - or any struct/class with a refernce member - cannot be default-constructed. A reference always must be bound to a valid object, you cannot leave them unitialized.
1
2
3
int value = 42;
Foo f{value}; // OK
Foo g; // Error: no default constructor available
Other default operations behave differently as well. A reference cannot be reseated, so copy and move assignments cannot rebind it. If they exist - they are not generated by the compiler by the default. A copy constructor will just copy the reference itself, leaving both the old and the new object referring to the same underlying variable.
1
2
3
4
5
6
7
8
9
10
11
int a = 10;
int b = 20;
Foo f1{a};
Foo f2{b};
f2 = f1; // Error: no copy or move assignments are available
Foo f3{f1}
std::cout << f3.x << '\n'; // prints 10
There are legitimate use cases for reference members, though. They can serve well as non-owning views into other objects or enforce that a struct must always be tied to an existing resource. You just have be thoughtful about when and how to use them.
Is there a fourth one?
After Steve’s talk, I started wondering if there was a fourth one — the one that appears after a member function declaration.
1
2
3
4
5
struct Foo {
void bar() &; // can be called only on lvalues
void bar() &&; // can be called only on rvalues
};
This trailing & (and its cousin &&) is called a ref-qualifier. It tells the compiler which kind of object — an lvalue or an rvalue — is allowed to call the method. In other words, it controls how the function can be invoked based on the value category of the object.
Ref-qualifiers are most often used in fluent APIs or move-only types, where chaining or resource ownership rules matter.
For example:
1
2
3
4
5
6
7
8
9
10
struct Builder {
Builder&& setName(std::string n) && {
name = std::move(n);
return std::move(*this);
}
Builder& setName(std::string n) & = delete;
std::string name;
};
Here, setName can only be called on a temporary (Builder{}), not on an existing named object. This prevents accidental mutation of already-built instances while keeping the rvalue usage clean and expressive.
Ref-qualifiers are often misunderstood because they’re rare in everyday code, but they’re a powerful tool for API design. They let you express ownership intent, control chaining, and enforce correct usage patterns at compile time — all with a tiny & at the end of a function.
So maybe it’s not exactly a fourth meaning, but it certainly feels like a forgotten part of the reference world.
Conclusion
It’s fascinating and scary how one small symbol — & — can mean so many different things depending on where it appears. It can shape how we pass data into functions, how we work with objects locally, how our classes behave, and even how methods can or cannot be called.
Each use carries its own rules, intentions, and traps. Sometimes it’s about efficiency, sometimes about clarity, and sometimes about enforcing invariants. Understanding these nuances helps us write APIs that communicate better, avoid surprises, and make intent explicit — both for ourselves and for others reading our code.
So next time you see an &, take a moment to think: is it about aliasing, ownership, construction, or invocation? It’s the same symbol, but each meaning tells a different story about how your program thinks about data and identity.
Connect deeper
If you liked this article, please
- hit on the like button,
- subscribe to my newsletter
- and let’s connect on Twitter!
