In lesson 10.8 -- Type
deduction for objects using the auto keyword, we discussed how the auto keyword can be used
to have the compiler deduce the type of a variable from the initializer:
We also noted that by default, type deduction will drop const from types:
Const (or constexpr) can be reapplied by adding the const (or constexpr) qualifier to
the definition of the deduced type:
Type deduction drops references
In addition to dropping const, type deduction will also drop references:
In the above example, variable ref is using type deduction. Although function
getRef() returns a std::string&, the reference qualifier is dropped, so the type
of ref is deduced as std::string.
Just like with dropped const, if you want the deduced type to be a reference, you can reapply the
reference at the point of definition:
Top-level const and low-level const
A top-level const is a const qualifier that applies to an object itself. For example:
In contrast, a low-level const is a const qualifier that applies to the object being referenced or pointed to:
A reference to a const value is always a low-level const. A pointer can have a top-level, low-level, or both kinds of const:
When we say that type deduction drops const qualifiers, it only drops top-level consts. Low-level consts are not dropped. We’ll see examples of this in just a moment.
Type deduction and const references
If the initializer is a reference to const, the reference is dropped first (and then reapplied if applicable), and then any top-level const is dropped from the result.
In the above example, since getConstRef() returns a const std::string&, the
reference is dropped first, leaving us with a const std::string. This const is now a top-level
const, so it is also dropped, leaving the deduced type as std::string.
Key insight
Dropping a reference may change a low-level const to a top-level const: const std::string&
is a low-level const, but dropping the reference yields const std::string, which is a top-level
const.
We can reapply a reference and/or const:
We covered the case for ref1 in the prior example. For ref2, this is similar to the
ref1 case, except we’re reapplying the const qualifier, so the deduced type is
const std::string.
Things get more interesting with ref3. Normally the reference would be dropped first, but since
we’ve reapplied the reference, it is not dropped. That means the type is still
const std::string&. And since this const is a low-level const, it is not dropped. Thus the
deduced type is const std::string&.
The ref4 case works similarly to ref3, except we’ve reapplied the const
qualifier as well. Since the type is already deduced as a reference to const, us reapplying const
here is redundant. That said, using const here makes it explicitly clear that our result will be
const (whereas in the ref3 case, the constness of the result is implicit and not obvious).
Best practice
If you want a const reference, reapply the const qualifier even when it’s not strictly
necessary, as it makes your intent clear and helps prevent mistakes.
What about constexpr references?
Constexpr is not part of an expression’s type, so it is not deduced by auto.
A reminder
When defining a const reference (e.g. const int&), the const applies to the object being
referenced, not the reference itself.
When defining a constexpr reference to a const variable (e.g. constexpr const int&), we need
to apply both constexpr (which applies to the reference) and const (which applies to
the type being referenced).
This is covered in lesson 12.4 -- Lvalue references to const.
Type deduction and pointers
Unlike references, type deduction does not drop pointers:
We can also use an asterisk in conjunction with pointer type deduction (auto*) to make it clearer
that the deduced type is a pointer:
Key insight
The reason that references are dropped during type deduction but pointers are not dropped is because references and pointers have different semantics.
When we evaluate a reference, we’re really evaluating the object being referenced. Therefore, when deducing a
type, it makes sense that we should deduce the type of the thing being referenced, not the reference itself.
Also, since we deduce a non-reference, it’s really easy to make it a reference by using
auto&. If type deduction were to deduce a reference instead, the syntax for removing a
reference if we didn’t want it is much more complicated.
On the other hand, pointers hold the address of an object. When we evaluate a pointer, we are evaluating the pointer, not the object being pointed to (if we want that, we can dereference the pointer). Therefore, it makes sense that we should deduce the type of the pointer, not the thing being pointed to.
The difference between auto and auto* Optional
When we use auto with a pointer type initializer, the type deduced for auto includes
the pointer. So for ptr1 above, the type substituted for auto is
std::string*.
When we use auto* with a pointer type initializer, the type deduced for auto does not
include the pointer -- the pointer is reapplied afterward after the type is deduced. So for ptr2
above, the type substituted for auto is std::string, and then the pointer is
reapplied.
In most cases, the practical effect is the same (ptr1 and ptr2 both deduce to
std::string* in the above example).
However, there are a couple of difference between auto and auto* in practice. First,
auto* must resolve to a pointer initializer, otherwise a compile error will result:
This makes sense: in the ptr4 case, auto deduces to std::string, then
the pointer is reapplied. Thus ptr4 has type std::string*, and we can’t initialize a
std::string* with an initializer that is not a pointer.
Second, there are differences in how auto and auto* behave when we introduce
const into the equation. We’ll cover this below.
Type deduction and const pointers Optional
Since pointers aren’t dropped, we don’t have to worry about that. But with pointers, we have both the const
pointer and the pointer to const cases to think about, and we also have auto vs auto*.
Just like with references, only top-level const is dropped during pointer type deduction.
Let’s start with a simple case:
When we use either auto const or const auto, we’re saying, “make the deduced pointer
a const pointer”. So in the case of ptr1 and ptr2, the deduced type is
std::string*, and then const is applied, making the final type std::string* const.
This is similar to how const int and int const mean the same thing.
However, when we use auto*, the order of the const qualifier matters. A const on the
left means “make the deduced pointer a pointer to const”, whereas a const on the right means “make
the deduced pointer type a const pointer”. Thus ptr3 ends up as a pointer to const, and
ptr4 ends up as a const pointer.
Now let’s look at an example where the initializer is a const pointer to const.
The ptr1 and ptr2 cases are straightforward. The top-level const (the const on the
pointer itself) is dropped. The low-level const on the object being pointed to is not dropped. So in both cases,
the final type is const std::string*.
The ptr3 and ptr4 cases are also straightforward. The top-level const is dropped, but
we’re reapplying it. The low-level const on the object being pointed to is not dropped. So in both cases, the
final type is const std::string* const.
The ptr5 and ptr6 cases are analogous to the cases we showed in the prior example. In
both cases, the top-level const is dropped. For ptr5, the auto* const reapplies the
top-level const, so the final type is const std::string* const. For ptr6, the
const auto* applies const to the type being pointed to (which in this case was already const), so
the final type is const std::string*.
In the ptr7 case, we’re applying the const qualifier twice, which is disallowed, and will cause a
compile error.
And finally, in the ptr8 case, we’re applying const on both sides of the pointer (which is allowed
since auto* must be a pointer type), so the resulting types is
const std::string* const.
Best practice
If you want a const pointer, pointer to const, or const pointer to const, reapply the const
qualifier(s) even when it’s not strictly necessary, as it makes your intent clear and helps prevent mistakes.
Tip
Consider using auto* when deducing a pointer type. Using auto* in this case makes
it clearer that we are deducing a pointer type, enlists the compiler’s help to ensure we don’t deduce a
non-pointer type, and gives you more control over const.
Summary
Sorry to hear about your headache. Let’s recap the most important points quickly.
Top-level vs low-level const:
- A top-level const applies to the object itself (e.g.
const int xorint* const ptr). - A low-level const applies to the object accessed through a reference or pointer (e.g.
const int& ref,const int* ptr).
What type deduction deduces:
- Type deduction first drops any references (unless the deduced type is defined as a reference). For a const reference, dropping the reference will cause the (low-level) const to become a top-level const.
- Type deduction then drops any top-level const (unless the deduced type is defined as
constorconstexpr). - Constexpr is not part of the type system, so is never deduced. It must always be explicitly applied to the deduced type.
- Type deduction does not drop pointers.
- Always explicitly define the deduced type as a reference,
const, orconstexpr(as applicable), and even if these qualifiers are redundant because they would be deduced. This helps prevent errors and makes it clear what your intent is.
Type deduction and pointers:
- When using
auto, the deduced type will be a pointer only if the initializer is a pointer. When usingauto*, the deduced type is always a pointer, even if the initializer is not a pointer. auto constandconst autoboth make the deduced pointer a const pointer. There is no way to explicitly specify a low-level const (pointer-to-const) usingauto.auto* constalso makes the deduced pointer a const pointer.const auto*makes the deduced pointer a pointer-to-const. If these are hard to remember,int* constis a const pointer (to int), soauto* constmust be a const pointer.const int*is a pointer-to-const (int), soconst auto*must be a pointer-to-const)- Consider using
auto*overautowhen deducing a pointer type, as it allows you to explicitly reapply both the top-level and low-level const, and will error if a pointer type is not deduced.