Why does a const reference to a reference lose its constness?
I always advise people to adopt the East-const style of always putting the const
to the right (east) of what it qualifies, and you just hit a situation where it makes all the difference.
Using West-const notation, you have written:
template<typename T>
void modify(const T &j){ j = 42; } // j has type int&
Substituting int&
for T
, you are naively performing textual substitution, as if it were a macro, and thinking of j
as being const int && j
. And it's not.
Using East-const notation, you would write:
template<typename T>
void modify(T const& j){ j = 42; }
Substituting int&
for T
, you get int& const& j
: notice how the const
is not at all where you thought it was?
And now, the world makes sense again. You have a const-reference to a reference to int
:
- The reference is
const
, so you cannot modify the reference itself... but then again you never can. - The
int
is NOTconst
, so you can modify theint
.
CQFD.
There is no such thing as reference to a reference i.e. there is no T & &
.
Given a const T&
where T
is int&
, the type collapses into int&
.
What happens to const?
There is no such thing as a const reference either i.e. there is no T & const
(not to be confused with reference to const, which does exist and which is quite often colloquially called const reference). No reference can be modified (which would be different from modifying the referred object), so constness is not meaningful. The const is simply ignored here.
Standard rule (from latest draft):
[dcl.ref] If a typedef-name ([dcl.typedef], [temp.param]) or a decltype-specifier ([dcl.type.simple]) denotes a type TR that is a reference to a type T, an attempt to create the type “lvalue reference to cv TR” creates the type “lvalue reference to T”, while an attempt to create the type “rvalue reference to cv TR” creates the type TR. [ Note: This rule is known as reference collapsing. — end note ]
Here cv refers to cv-qualifiers i.e. const and volatile.
To clarify why this applies to template arguments:
[temp.param] A type-parameter whose identifier does not follow an ellipsis defines its identifier to be a typedef-name ...
P.S. The way reference collapsing is specified is part of the reason why perfect forwarding works.
Given const T&
, const
is qualified on T
itself. When T
is int&
, const T
means const
reference (i.e. int& const
); there's no const
reference in fact, (note that it's not reference to const
i.e. const int&
), references can't be rebound after being initialized. In this case the const
qualifier is just ignored.
Reference types cannot be cv-qualified at the top level; there is no syntax for that in declaration, and if a qualification is added to a typedef-name or decltype specifier, or type template parameter, it is ignored.