87

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?

Mat
  • 202,337
  • 40
  • 393
  • 406
Troskyvs
  • 7,537
  • 7
  • 47
  • 115
  • 3
    Also, `boolalpha(cout)` is very unusual. You could do `std::cout << boolalpha` instead. – isanae Jun 27 '16 at 02:54
  • 6
    So much for `ri` being an "alias" indistinguishable from `i`. – Kaz Jun 27 '16 at 04:29
  • 1
    This is because a reference is always bounded the same object. `i` is also a reference but for historical reasons you don't declare it as such in a explicit way. Thus `i` is reference that refers to a storage and `ri` is a reference that refers to the same storage. But there is no difference in nature in between `i` and `ri`. As you can't change the binding of a reference, there is no need to qualify it as `const`. And let me claim that @Kaz comment is far much better that the validated answer (never explain references using pointers, a ref is a name, a ptr is a variable). – Jean-Baptiste Yunès Jun 27 '16 at 16:29
  • 1
    Fantastic question. Prior to seeing this example, I would have expected `is_const` to return `true` in this case, too. In my opinion, this is a good example of why `const` is fundamentally backwards; a "mutable" attribute (a la Rust's `mut`) seems like it would be more consistent. – Kyle Strand Jun 27 '16 at 20:05
  • @Kaz It is indistinguishable except for as the operand of `decltype` (which has reflective behaviour, outside of the usual rules for expressions) – M.M Jun 27 '16 at 23:05
  • 1
    Perhaps the title should be changed to "why is `is_const::value` false?" or similar; I'm struggling to see any meaning to the question other than asking about the behaviour of type traits – M.M Jun 27 '16 at 23:16
  • @M.M: And except that its life may end either before or after the referent (if either lifetime has ended, using the reference will likely be undefined behavior) – Ben Voigt Jun 27 '16 at 23:25

6 Answers6

55

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.

Galik
  • 47,303
  • 4
  • 80
  • 117
  • 13
    It sounds like you're saying "references can't be const because they're always const"? – ruakh Jun 27 '16 at 02:27
  • @ruakh I was sort of trying for "references are never const because they don't need to be". Its not their being `const` that prevents them from being rebound it is the fact that *semantically* all actions upon them after they are bound are transferred to the object they refer to, so what would re-binding a reference even look like? – Galik Jun 27 '16 at 02:39
  • 2
    It may be true that "*semantically* all actions upon them after they are bound are transferred to the object they refer to", but the OP was expecting that to apply to `decltype` as well, and found that it did not. – ruakh Jun 27 '16 at 02:49
  • @ruakh I think you are right the OP may be expecting `decltype` to transfer to the bound object, I added a note about that too. – Galik Jun 27 '16 at 02:59
  • 11
    There is a lot of meandering supposition and dubiously applicable analogies to pointers here, which I think confuses the issue more than necessary, when checking the standard like sonyuanyao did gets straight to the real point: A reference is not a cv-qualifiable type, therefore it can't be `const`, therefore `std::is_const` must return `false`. They could've instead used wording that meant it must return `true`, but they didn't. That's it! All this stuff about pointers, "I assume", "I suppose", etc doesn't provide any real elucidation. – underscore_d Jun 27 '16 at 16:04
  • To expand slightly on @ruakh's comment, references are *not* treated "semantically like pointers"; they are treated like *semantic sugar over* pointers. So if `ri` is a pointer, then the analogous `is_const` expression expected by the OP is actually `is_const::value` (i.e. the pointer is dereferenced, as if `decltype()` were a function). – Kyle Strand Jun 27 '16 at 16:08
  • @KyleStrand I didn't say that references were "semantically like pointers" I said they are semantically *different* to pointers, but that they were treated *syntactically* like pointers. Both pointers and references are used for *indirection* so the syntax needs to be able to differentiate between application to the pointer/reference and application to the thing the pointer/reference refers to. I feel that pointing out where the syntax is similar helps to explain **why** the syntax used in OPs question does not make the reference itself const, only the object being referred to. – Galik Jun 27 '16 at 16:15
  • @Galik Bleargh, sorry, I meant for that to be a direct quote--"syntactically like pointers." I would argue that references *are* **semantically** like pointers--that is, pointers are a good way to understand references (and references are often (always?) implemented as pointers). But they *are not* "syntactically" like pointers, because the *syntax is different* when using references. – Kyle Strand Jun 27 '16 at 16:20
  • @KyleStrand and although they often are implemented as pointers when not optimised away, references are not required to bear any resemblance to pointers. The frequency of people who've been told the at-best-oversimplification that 'a reference is a pointer with automatic dereferencing' leads to so much unnecessary confusion that I don't think it's worth adding to. – underscore_d Jun 27 '16 at 16:22
  • 1
    @underscore_d This is probably a better question for chat than for the comments section here, but do you have an example of "unnecessary confusion" from this way of thinking about references? If they can be implemented that way, what's the problem with thinking of them that way? (I don't think this question counts because `decltype` is not a runtime operation, so the "at runtime, references behave like pointers" idea, whether correct or not, doesn't really apply. – Kyle Strand Jun 27 '16 at 16:38
  • 1
    References aren't treated syntactically like pointers, either. They have different syntax to declare and to use. So I'm not even sure what you mean. – Jordan Melo Jun 27 '16 at 20:28
  • @JordanMelo If you consult the *formal grammar* in the `C++11` Specification you will find the syntax for declaring *pointers* (8.3.1/1) and *references* (8.3.2/1) is *identical* with the exception of the (optional) *cv-qualifier-seq* as noted in my answer. – Galik Jun 28 '16 at 01:58
  • @Galik Uh, except for the fact that pointer declarations *require* `*` whereas reference declarations *require* `&` or `&&`. This is not just a syntactic difference; this is an *enormous* syntactic difference and one of the most fundamental deviations from C syntax. – Kyle Strand Jun 28 '16 at 19:44
  • @KyleStrand please be serious. – Galik Jun 28 '16 at 19:46
  • @Galik I don't understand. You claimed that "the syntax...is *identical*" (emphasis yours). This is just flat-out obviously untrue. – Kyle Strand Jun 28 '16 at 19:46
  • ....unless you don't think that `*`, `&`, and `&&` are *syntactic* elements of the language....? I can't imagine why you would think that, though. – Kyle Strand Jun 28 '16 at 19:47
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/115882/discussion-between-kyle-strand-and-galik). – Kyle Strand Jun 28 '16 at 19:48
  • 1
    In the interest of complete clarity, here's a corrected version of my original comment: References are not treated "syntactically like pointers"; they are treated like *syntactic sugar over* pointers. So if `ri` is a pointer, then the analogous `is_const` expression expected by the OP is actually `is_const::value` (i.e. the pointer is dereferenced, as if `decltype()` were a function). – Kyle Strand Jun 28 '16 at 20:04
  • @underscore_d I would still be interested in knowing why you think that it's an oversimplification (or incorrect) to think of references as pointers. – Kyle Strand Nov 21 '16 at 21:53
55

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.

songyuanyao
  • 169,198
  • 16
  • 310
  • 405
  • 5
    Straight to the Standard and hence straight to the point, rather than all the waffling supposition of the accepted answer. – underscore_d Jun 27 '16 at 15:31
  • 5
    @underscore_d This is a fine answer until you recognize that the op was also asking why. "Because it says so" is not really useful to that aspect of the question. – simpleuser Jun 27 '16 at 15:49
  • 6
    @user9999999 The other answer doesn't really answer that aspect either. If a reference can't be rebound, why doesn't `is_const` return `true`? That answer tries to draw an analogy to how pointers are optionally reseatable, whereas references aren't - and in doing so, leads to self-contradiction for the same reason. I'm not sure there's a real explanation either way, other than a somewhat arbitrary decision by those writing the Standard, and sometimes that's the best we can hope for. Hence this answer. – underscore_d Jun 27 '16 at 16:00
  • 2
    Another important aspect, I think, is that `decltype` is *not a function* and therefore *works directly on the reference itself* rather than on the referred-to object. (This is perhaps more relevant to the "references are basically pointers" answer, but I still think it's part of what makes this example confusing and therefore worth mentioning here.) – Kyle Strand Jun 27 '16 at 16:09
  • 3
    To further elucidate the comment from @KyleStrand: `decltype(name)` acts differently from a general `decltype(expr)`. So for example, `decltype(i)` is the declared type of `i` which is `const int`, while `decltype((i))` would be `int const &`. – Daniel Schepler Jun 27 '16 at 19:31
  • 1
    I would also mention that `decltype` has peculiar behaviour, e.g. compare `is_const::value` with `is_const::value`, which differ even though `i` and `*&i` are exactly identical in all contexts other than `decltype`. – M.M Jun 27 '16 at 23:13
  • 1
    @M.M: "`i` and `*&i` are exactly identical in all contexts other than `decltype`" This may be true for the example, where `const int i;`, but it is not true in general, since `&` and `*` are both overloadable operators. Daniel's `i` vs `(i)` is much better for the purposes of this illustration. – Ben Voigt Jun 27 '16 at 23:28
  • 1
    @BenVoigt Right, I should also say "except when `operator&` is overloaded", or maybe `std::addressof`. (Not possible to overload `operator*` for pointers) – M.M Jun 27 '16 at 23:29
  • @M.M Intriguingly (i.e. annoyingly), there are [other contexts that, due to compiler bugs, treat `i` differently from `*&i`.](http://stackoverflow.com/a/34905922/1858225) – Kyle Strand Jun 28 '16 at 19:40
  • @KyleStrand I don't see `*&` anywhere in that linked question – M.M Jun 29 '16 at 03:53
  • @M.M It's mentioned in the last edit, in a comment, and in the answer as a way to coerce GCC into behaving correctly despite the bug. – Kyle Strand Jun 29 '16 at 04:26
  • @KyleStrand Ctrl-F for `*&` finds no results for me.. you're not mixing up `*&` with `&*` ? – M.M Jun 29 '16 at 06:42
  • @M.M Sorry, you're right, I did mix those up. I even thought I double-checked after your last comment. That said, I'm now curious whether `*&` would also work. – Kyle Strand Jun 29 '16 at 16:28
18

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.

Community
  • 1
  • 1
chema989
  • 3,962
  • 2
  • 20
  • 33
9

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.

Lightness Races in Orbit
  • 378,754
  • 76
  • 643
  • 1,055
6

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.

Kaz
  • 55,781
  • 9
  • 100
  • 149
  • You've got a lot of interesting and helpful information here (+1), but it's more or less limited to the fact that "references are in the type system"--this doesn't entirely answer OP's question. Between this answer and @songyuanyao's answer, I'd say there's more than enough information to understand the situation, but it feels like the necessary *background* for an answer rather than a complete answer. – Kyle Strand Jun 27 '16 at 19:58
  • 1
    Also, I think this sentence is worth highlighting: "When you use ri, the reference is transparently resolved..." One key point (that I've mentioned in comments but which so far hasn't shown up in any of the answers) is that `decltype` **does not** perform this transparent resolution (it's not a function, so `ri` isn't "used" in the sense you describe). This fits in very nicely with your whole focus on *the type system* -- they key connection is that `decltype` is *a type-system operation*. – Kyle Strand Jun 27 '16 at 20:00
  • Hmm, the stuff about arrays feels like a tangent, but the final sentence is helpful. – Kyle Strand Jun 28 '16 at 20:34
  • .....though at this point I've already upvoted you and I'm not the OP, so you're not really gaining or losing anything by listening to my critiques.... – Kyle Strand Jun 28 '16 at 20:34
  • Obviously an easy (and correct answer) just points us to the standard but I like the background thinking here even though some might argue that parts of it are supposition. +1. – Steve Kidd Jun 29 '16 at 11:50
-6

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

And see std::is_const.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Sotter.liu
  • 115
  • 4
  • 10