Why are references not "const" in C++?

C++ReferenceConstantsLanguage LawyerDecltype

C++ Problem Overview


We know that a "const variable" indicates that once assigned, you cannot change the variable, like this:

int const i = 1;
i = 2;

The program above will fail to compile; gcc prompts with an error:

assignment of read-only variable 'i'

No problem, I can understand it, but the following example is beyond my understanding:

#include<iostream>
using namespace std;
int main()
{
    boolalpha(cout);
    int const i = 1;
    cout << is_const<decltype(i)>::value << endl;
    int const &ri = i;
    cout << is_const<decltype(ri)>::value << endl;
    return 0;
}

It outputs

true
false

Weird. We know that once a reference is bound to a name/variable, we cannot change this binding, we change its bound object. So I suppose the type of ri should be the same as i: when i is an int const, why is ri not const?

C++ Solutions


Solution 1 - C++

This may seem counter-intuitive but I think the way to understand this is to realize that, in certain respects, references are treated syntactically like pointers.

This seems logical for a pointer:

int main()
{
	boolalpha(cout);

	int const i = 1;
	cout << is_const<decltype(i)>::value << endl;

	int const* ri = &i;
	cout << is_const<decltype(ri)>::value << endl;
}

Output:

true
false

This is logical because we know it is not the pointer object that is const (it can be made to point elsewhere) it is the object that is being pointed to.

So we correctly see the constness of the pointer itself returned as false.

If we want to make the pointer itself const we have to say:

int main()
{
	boolalpha(cout);

	int const i = 1;
	cout << is_const<decltype(i)>::value << endl;

	int const* const ri = &i;
	cout << is_const<decltype(ri)>::value << endl;
}

Output:

true
true

And so I think we see a syntactic analogy with the reference.

However references are semantically different to pointers especially in one crucial respect, we are not allowed to rebind a reference to another object once bound.

So even though references share the same syntax as pointers the rules are different and so the language prevents us from declaring the reference itself const like this:

int main()
{
	boolalpha(cout);

	int const i = 1;
	cout << is_const<decltype(i)>::value << endl;

	int const& const ri = i; // COMPILE TIME ERROR!
	cout << is_const<decltype(ri)>::value << endl;
}

I assume we are not allowed to do this because it doesn't appear to be needed when the language rules prevent the reference from being rebound in the same way a pointer could(if it is not declared const).

So to answer the question:

>Q) Why “reference” is not a “const” in C++?

In your example the syntax makes the thing being referred to const the same way it would if you were declaring a pointer.

Rightly or wrongly we are not allowed to make the reference itself const but if we were it would look like this:

int const& const ri = i; // not allowed

> Q) we know once a reference is bind to a name/variable, we cannot change this binding, we change its binded object. So I suppose the type of ri should be same as i: when i is a int const, why ri is not const?

Why is the decltype() not transferred to the object the referece is bound to?

I suppose this is for semantic equivalence with pointers and maybe also the function of decltype() (declared type) is to look back at what was declared before the binding took place.

Solution 2 - C++

> why is "ri" not "const"?

std::is_const checks whether the type is const-qualified or not.

> If T is a const-qualified type (that is, const, or const volatile), provides the member constant value equal true. For any other type, value is false.

But the reference can't be const-qualified. References [dcl.ref]/1

> Cv-qualified references are ill-formed except when the cv-qualifiers > are introduced through the use of a typedef-name ([dcl.typedef], > [temp.param]) or decltype-specifier ([dcl.type.simple]), in which case > the cv-qualifiers are ignored.

So is_const<decltype(ri)>::value will return false becuase ri (the reference) is not a const-qualified type. As you said, we can't rebind a reference after initialization, which implies reference is always "const", on the other hand, const-qualified reference or const-unqualified reference might not make sense actually.

Solution 3 - C++

You need to use std::remove_reference for get the value you're looking for.

std::cout << std::is_const<std::remove_reference<decltype(ri)>::type>::value << std::endl;

For more information, see this post.

Solution 4 - C++

Why are macros not const? Functions? Literals? The names of types?

const things are only a subset of immutable things.

Since reference types are just that — types — it may have made some sense to require the const-qualifier on them all for symmetry with other types (particularly with pointer types), but this would get very tedious very quickly.

If C++ had immutable objects by default, requiring the mutable keyword on anything you didn't want to be const, then this would have been easy: simply don't allow programmers to add mutable to reference types.

As it is, they are immutable without qualification.

And, since they are not const-qualified, it would probably be more confusing for is_const on a reference type to yield true.

I find this to be a reasonable compromise, especially since the immutability is anyway enforced by the mere fact that no syntax exists to mutate a reference.

Solution 5 - C++

This is a quirk/feature in C++. Although we don't think of references as types, they in fact "sit" in the type system. Although this seems awkward (given that when references are used, the reference semantics occurs automatically and the reference "gets out of the way"), there are some defensible reasons why references are modeled in the type system instead of as a separate attribute outside of type.

Firstly, let us consider that not every attribute of a declared name must be in the type system. From the C language, we have "storage class", and "linkage". A name can be introduced as extern const int ri, where the extern indicates static storage class and the presence of linkage. The type is just const int.

C++ obviously embraces the notion that expressions have attributes that are outside of the type system. The language now has a concept of "value class" which is an attempt to organize the growing number of non-type attributes that an expression can exhibit.

Yet references are types. Why?

It used to be explained in C++ tutorials that a declaration like const int &ri introduced ri as having type const int, but reference semantics. That reference semantics was not a type; it was simply a kind of attribute indicating an unusual relationship between the name and the storage location. Furthermore, the fact that references are not types was used to rationalize why you cannot construct types based on references, even though the type construction syntax allows it. For instance, arrays or pointers to references not being possible: const int &ari[5] and const int &*pri.

But in fact references are types and so decltype(ri) retrieves some reference type node which is unqualified. You must descend past this node in the type tree to get to the underlying type with remove_reference.

When you use ri, the reference is transparently resolved, so that ri "looks and feels like i" and can be called an "alias" for it. In the type system, though, ri does in fact have a type which is "reference to const int".

Why are references types?

Consider that if references were not types, then these functions would be considered to have the same type:

void foo(int);
void foo(int &);

That simply cannot be for reasons which are pretty much self-evident. If they had the same type, that means either declaration would be suitable for either definition, and so every (int) function would have to be suspected of taking a reference.

Similarly, if references weren't types, then these two class declarations would be equivalent:

class foo {
  int m;
};

class foo {
  int &m;
};

It would be correct for one translation unit to use one declaration, and another translation unit in the same program to use the other declaration.

The fact is that a reference implies a difference in implementation and it is impossible to separate that from type, because type in C++ has to do with the implementation of an entity: its "layout" in bits so to speak. If two functions have the same type, they can be invoked with the same binary calling conventions: the ABI is the same. If two structs or classes have the same type, their layout is the same as well as the semantics of access to all the members. The presence of references changes these aspects of types, and so it's a straightforward design decision to incorporate them into the type system. (However, note a counterargument here: a struct/class member can be static, which also changes the representation; yet that isn't type!)

Thus, references are in the type system as "second class citizens" (not unlike functions and arrays in ISO C). There are certain things we cannot "do" with references, such as declare pointers to references, or arrays of them. But that doesn't mean they aren't types. They just aren't types in a way that it makes sense.

Not all these second-class-restrictions are essential. Given that there are structures of references, there could be arrays of references! E.g.

// fantasy syntax
int x = 0, y = 0;
int &ar[2] = { x, y };

// ar[0] is now an alias for x: could be useful!

This just isn't implemented in C++, that's all. Pointers to references do not make sense at all, though, because a pointer lifted from a reference just goes to the referenced object. The likely reason why there are no arrays of references is that the C++ people consider arrays to be a kind of low-level feature inherited from C that is broken in many ways that are irreparable, and they don't want to touch arrays as the basis for anything new. The existence of arrays of references, though, would make a clear example of how references have to be types.

Non-const-qualifiable types: found in ISO C90, too!

Some answers are hinting at the fact that references don't take a const qualifier. That is rather a red herring, because the declaration const int &ri = i isn't even attempting to make a const-qualified reference: it's a reference to a const-qualified type (which is itself not const). Just like const in *ri declares a pointer to something const, but that pointer is itself not const.

That said, it is true that references cannot carry the const qualifier themselves.

Yet, this is not so bizarre. Even in the ISO C 90 language, not all types can be const. Namely, arrays cannot be.

Firstly, the syntax doesn't exist for declaring a const array: int a const [42] is erroneous.

However, what the above declaration is trying to do can be expressed via an intermediate typedef:

typedef int array_t[42];
const array_t a;

But this doesn't do what it looks like it does. In this declaration, it is not a which gets const qualified, but the elements! That is to say, a[0] is a const int, but a is just "array of int". Consequently, this doesn't require a diagnostic:

int *p = a; /* surprise! */

This does:

a[0] = 1;

Again, this underscores the idea that references are in some sense "second class" in the type system, like arrays.

Note how the analogy holds even more deeply, since arrays also have an "invisible conversion behavior", like references. Without the programmer having to use any explicit operator, the identifier a automatically turns into an int * pointer, as if the expression &a[0] had been used. This is analogous to how a reference ri, when we use it as a primary expression, magically denotes the object i to which it is bound. It's just another "decay" like the "array to pointer decay".

And just like we must not become confused by the "array to pointer" decay into wrongly thinking that "arrays are just pointers in C and C++", we likewise mustn't think that references are just aliases that have no type of their own.

When decltype(ri) suppresses the usual conversion of the reference to its referent object, this is not so different from sizeof a suppressing the array-to-pointer conversion, and operating on the array type itself to calculate its size.

Solution 6 - C++

const X& x” means x aliases an X object, but you can’t change that X object via x.

And see std::is_const.

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionTroskyvsView Question on Stackoverflow
Solution 1 - C++GalikView Answer on Stackoverflow
Solution 2 - C++songyuanyaoView Answer on Stackoverflow
Solution 3 - C++chema989View Answer on Stackoverflow
Solution 4 - C++Lightness Races in OrbitView Answer on Stackoverflow
Solution 5 - C++KazView Answer on Stackoverflow
Solution 6 - C++Sotter.liuView Answer on Stackoverflow