Auto Type Deduction with Pointers and References
Understand how auto interacts with pointer, reference, and const qualifiers.
What Is Type Deduction with Pointers, References, and Const?
auto deduces a variable's type from its initializer, but it does not copy that type wholesale. Some parts are deliberately discarded and others are kept, and which is which depends on references, pointers, and where exactly a const sits. This lesson gives you the rules and a way to check them yourself.
Two rules cover almost everything:
- A reference is dropped, unless you write it back with
auto&. - A top-level
constis dropped, unless you write it back withconst auto.
Everything else is a consequence of those two, plus knowing which const counts as top-level.
The Two Kinds of const
A top-level const applies to the object itself:
const int num; // const applies to num
int* const pointer; // const applies to pointer
A low-level const applies to whatever is being pointed at or referred to:
const int& reference; // const applies to the referenced object
const int* pointer; // const applies to the pointed-to object
A reference to const is always low-level. A pointer can carry either, or both at once:
const int* const pointer; // left const is low-level, right const is top-level
References have no top-level const syntax, because a reference is already unable to be rebound and is therefore implicitly top-level const.
This distinction is the whole game: type deduction drops only top-level const. Low-level const always survives.
Starting Simple
With a plain value initializer, const is dropped and can be reapplied:
#include <iostream>
int main()
{
constexpr double pi{ 3.14159 }; // constexpr implies const
auto value{ pi }; // double, const dropped
const auto locked{ pi }; // const double, const reapplied
constexpr auto fixed{ pi }; // constexpr double
std::cout << value << ' ' << locked << ' ' << fixed << '\n';
return 0;
}
Output:
3.14159 3.14159 3.14159
constexpr is worth noting separately: it is not part of a type at all, so auto never deduces it. If you want a constexpr variable, you write constexpr yourself, every time.
References work the same way. Given a function returning std::string&, auto gives you a std::string copy, and auto& gives you the reference back.
Combining References and const
The interesting case is a function returning a reference to const, because dropping the reference changes what kind of const you are left with.
Dropping a reference can promote a low-level const into a top-level one.
const std::string& has a low-level const. Remove the reference and you have const std::string, where that same const is now top-level, and therefore droppable.
That two-step explains the whole table below. The reference goes first, then any const that is now top-level:
| Declaration | Deduced type | Why |
|---|---|---|
auto message1 |
std::string |
Reference dropped, const became top-level and was dropped too |
const auto message2 |
const std::string |
Same, then const reapplied |
auto& message3 |
const std::string& |
Reference reapplied, so the const stays low-level and survives |
const auto& message4 |
const std::string& |
Same as above, with a redundant but clarifying const |
You do not have to take that on trust. static_assert with std::is_same_v checks each row at compile time, so if any claim were wrong the program would not build:
#include <iostream>
#include <string>
#include <type_traits>
const std::string& getTextConstRef()
{
static const std::string text{ "beacon" };
return text;
}
int main()
{
auto message1{ getTextConstRef() };
const auto message2{ getTextConstRef() };
auto& message3{ getTextConstRef() };
const auto& message4{ getTextConstRef() };
static_assert(std::is_same_v<decltype(message1), std::string>);
static_assert(std::is_same_v<decltype(message2), const std::string>);
static_assert(std::is_same_v<decltype(message3), const std::string&>);
static_assert(std::is_same_v<decltype(message4), const std::string&>);
std::cout << "All four deductions match: " << message1 << '\n';
return 0;
}
Output:
All four deductions match: beacon
The message4 row shows the habit worth forming. Its const is redundant, since message3 already produced a const reference, but writing it states the intent instead of leaving a reader to work out that the constness came along for the ride.
Reapply
const, constexpr, and & whenever you want them, even when deduction would have supplied them anyway. Redundant qualifiers cost nothing and make the intent unmistakable.
Why Pointers Are Not Dropped
Unlike references, pointers survive deduction untouched. auto p{ getPointer() }; gives you a pointer.
The asymmetry follows from what the two things mean. Evaluating a reference gives you the referenced object, so deducing the object's type is the natural reading, and
auto& is right there when you want the reference. Evaluating a pointer gives you the pointer itself, not its target, so deducing a pointer is the natural reading. Dereference it if the target is what you meant.
auto Versus auto*
Both forms usually produce the same type, but they get there differently. With auto, the deduced type includes the pointer. With auto*, the deduced type is the pointed-to type and the pointer is reattached afterwards.
Two consequences follow. First, auto* demands a pointer initializer, so auto* p{ *getPointer() }; is a compile error while auto p{ *getPointer() }; happily deduces std::string. That is a feature: it makes the compiler confirm you got a pointer.
Second, the two forms differ in how const attaches, which is the next section.
const With Pointers
With auto, a const on either side means the same thing, exactly as const int and int const do: make the pointer itself const. There is no way to express a pointer-to-const with plain auto.
With auto*, the position matters. A const on the left makes it a pointer to const; a const on the right makes it a const pointer. If that is hard to hold onto, compare against the non-deduced spelling: const int* is a pointer to const, so const auto* is too, and int* const is a const pointer, so auto* const is as well.
Starting from a const std::string* const, which carries both kinds:
| Declaration | Deduced type | Top-level const | Low-level const |
|---|---|---|---|
auto pointer1 |
const std::string* |
Dropped | Kept |
auto* pointer2 |
const std::string* |
Dropped | Kept |
const auto pointer3 |
const std::string* const |
Reapplied | Kept |
auto* const pointer4 |
const std::string* const |
Reapplied | Kept |
const auto* pointer5 |
const std::string* |
Dropped | Kept, and explicitly restated |
const auto* const pointer6 |
const std::string* const |
Reapplied | Kept, and explicitly restated |
Verified the same way:
#include <iostream>
#include <string>
#include <type_traits>
int main()
{
std::string text{ "beacon" };
const std::string* const pointer{ &text };
auto pointer1{ pointer };
auto* pointer2{ pointer };
const auto pointer3{ pointer };
auto* const pointer4{ pointer };
const auto* pointer5{ pointer };
const auto* const pointer6{ pointer };
static_assert(std::is_same_v<decltype(pointer1), const std::string*>);
static_assert(std::is_same_v<decltype(pointer2), const std::string*>);
static_assert(std::is_same_v<decltype(pointer3), const std::string* const>);
static_assert(std::is_same_v<decltype(pointer4), const std::string* const>);
static_assert(std::is_same_v<decltype(pointer5), const std::string*>);
static_assert(std::is_same_v<decltype(pointer6), const std::string* const>);
std::cout << "Pointer deductions verified: " << *pointer1 << '\n';
return 0;
}
Output:
Pointer deductions verified: beacon
One combination is simply illegal: const auto const p{ pointer }; applies the qualifier twice and fails to compile. Use const auto* const when you want both.
Prefer
auto* when you are deducing a pointer. It states that a pointer is expected, makes the compiler enforce it, and lets you place const on either side to say precisely which one you mean.
Summary
Two kinds of const: a top-level const applies to the object itself (const int, int* const), while a low-level const applies to what is pointed at or referred to (const int&, const int*). A pointer can have both.
What deduction drops: references first, then any const that is top-level once the reference is gone. Low-level const is never dropped. Pointers are never dropped.
Reference plus const: dropping a reference can turn a low-level const into a top-level one, which is why auto on a const std::string& yields a plain std::string, while auto& yields const std::string&.
constexpr: not part of the type, never deduced, always written explicitly.
auto versus auto*: auto absorbs the pointer into the deduced type, auto* deduces the pointed-to type and reattaches the pointer, so it fails unless the initializer really is a pointer.
const placement: with auto, either side means a const pointer. With auto*, left means pointer-to-const and right means const pointer.
Habit: reapply &, const, and constexpr whenever you want them, redundant or not, and reach for auto* over auto for pointers.
How did you find this lesson?
Your rating helps us improve the content.
Create an account to track your progress and access interactive exercises. Already have one? Sign in.
Auto Type Deduction with Pointers and References - Quiz
Test your understanding of the lesson.
Practice Exercises
Type Deduction with References and Const
Master type deduction rules for auto with references, pointers, and const. Understand top-level versus low-level const and when to explicitly reapply qualifiers.
Lesson Discussion
Share your thoughts and questions
No comments yet. Be the first to share your thoughts!