Shouldn't "const references" be actually called "references to const"? - c++

I came up with this feeling when playing with constexpr references. But the issue itself is not related to constexpr, it is merely revealed by it.
We know that there are "pointers to const" and there are "const pointers". BTW, since the latter are much less used than the former, their name is often actually used to reference the former. Anyway, there is no this distinction for references, since they are not rebindable. Thus we only have "const references" and not "references to const". This is the common terminology and it is used in the standard as well. But it does not seem right to me.
Let's take a look at some well-known examples, commented with this common terminology:
int i = 0; // Value
int* pi = &i; // Pointer to i
const int* cpi = &i; // Pointer to const i
int const* cpi = &i; // Pointer to const i (east-const style)
int* const pci = &i; // Const pointer to i
int& ri = i; // Reference to i
const int& rci = i; // Const reference to i
int const& rci = i; // Const reference to i (east-const style)
Now let's take a closer look on several lines from the above:
const int* cpi = &i; // Pointer to const i
const int& rci = i; // Const reference to i
const int* declares "pointer to const", while const int& declares "const reference". Same order in the declaration, different order in the meaning. There is a clear inconsistency here. But if we call it "reference to const" inconsistency is gone. Same order on both sides.
The tension intensifies with the introduction of constexpr:
constexpr int& cri = i; // Constexpr reference to i
constexpr const int& crci = i; // Constexpr const reference to i
"Constexpr const" reference? Huh? The documentation clearly says:
A constexpr specifier used in an object declaration implies const.
A reference is an object. So why would we need const when it is already constepxr?.. Yet we need it, if i is const. Looks frustrating.
But the frustration is no more with help of "reference to const". Let's call crci "constexpr reference to const i". Now it is clear that const is applied to the referenced object, while constexpr is applied to the reference itself, stating the fact it can be used in constexpr context. And also that it is const, since all references are const. Makes perfect sense. To make it more consistent with the pointers, we might even have used the following hypothetical syntax:
const int& constexpr crci = i; // Constexpr reference to const i
const int* const pci = &i; // Const pointer to const i (perfect consistency)
But that's not how C++ works, for better or worse.
This concept of "reference to const" really helped me to understand "constexpr const reference" thing.
What do you think? Does the concept of "reference to const" sound convincing to you?

Yes, the proper name for const int & is a "reference to const int".
It's often called a "const reference to int" (probably because it sounds better to some), but technically that's a misnomer.
Even though they are not rebindable, references themselves are never const (i.e. std::is_const_v always equals to false for a reference).
constexpr const int& crci
"Constexpr const" reference? Huh? ... Let's call crci "constexpr reference to const i"
That's correct, crci is a "constexpr reference to const i".
To make it more consistent with the pointers, we might even have used the following hypothetical syntax:
const int& constexpr crci = i; // Constexpr reference to const i
const int* const pci = &i; // Const pointer to const i (perfect consistency)
To be honest, I don't see how this proposal improves consistency. int *constexpr pci; is not allowed for pointers, so why should it be allowed for references?
Things seem to be reasonably consistent to begin with:
constexpr const int *a; // constexpr (and const) pointer to const int
constexpr const int &a; // constexpr (but not const) reference to const int
Yes, adding constexpr doesn't make a reference itself const, but I don't think it needs to be fixed. (What would be the difference between a const refenrece and a non-const reference?)
Note that constexpr is not a part of a type, but it's a property of a variable. (e.g. constexpr int x; declares x with the type const int, and not some hypothetical constexpr int).

Since volatile tends to be also be relevant, the standard often uses "reference to a cv-(un)qualified" in normative text to cover both qualifiers, which conforms to your preference.
With quick search, besides the code example that you quoted, the standard appears to use (non-) const reference in following places, all in the standard library spec:
[container.node.observers]
key_type& key() const;
Returns: A non-const reference to ...
[rand.req.adapt]
... The expression a.base() shall be valid and shall return a const reference ...
[futures.shared.future
— (19.1) shared_future::get() returns a const reference to ...
Arguably, the wording could be improved by using reference to const.
Outside of the standard, "references to const" is used. But so is const reference, which is sometimes chosen possibly due to terseness.
In fact, pointers to const are also colloquially often called const pointers even though that meaning is ambiguous, which easily leads to confusion. In case of references, there is no ambiguity to those who know that references cannot be top level qualified. Beginners won't know that, so the use of "reference to const" is important in introductory material in my opinion. But so is perhaps explanation that const reference is used to mean the same thing.

Related

Why is the const qualifier in this type alias dropped? [duplicate]

This question already has answers here:
constant references with typedef and templates in c++
(5 answers)
Closed 4 years ago.
TL;DR
Given the following type:
struct A
{
std::vector<std::string> vec;
using reference = std::iterator_traits<decltype(vec)::iterator>::reference;
using const_reference = const reference;
};
Why is reference == const_reference? Why is the const qualifier dropped in the second type alias?
See the example on godbold which shouldn't compile.
Details
I have a templated class that takes a bunch of iterators (-types) as template arguments. From these iterators I need to deduce the reference and const reference type because I have some member functions like:
struct A
{
std::vector<std::string> vec;
using reference = std::iterator_traits<decltype(vec)::iterator>::reference;
using const_reference = const reference;
const_reference foo() const
{
return vec[0];
}
};
By dropping the const qualifier, I'm effectively returning a reference in foo which is illegal because it's a const member function and so the compiler throws.
It is dropped. What we call a "const reference" is really a reference to const - const int&.
Having an int& and adding const would make that int& const. But such a const is dropped.
Your problem is similar to the difference between const int* and int* const. Except that for references, int& and int& const is the same type - the const is ignored.
Your problem is west const. West const is bad const east const is best const.
West const is putting the const on the left of the token you want to be const. East const is putting it on the right.
If I told you never to put const on the left of your types, and that const always applies to the thing on its left, look at this:
using const_reference = reference const;
you can probably work out why the const didn't work. After expanding reference naively you get string&const -- here you attempt to apply const to & not string, and a foo &const is not the same as a foo const& -- a foo&const is just a foo& as const cannot apply to the reference itself, but only to the type referred to.
Sure, you say, but that is why I want west const!
West const does the same thing here. It applies to the & not then string and then is discarded. It just does so in a way that is more confusing and harder to grasp intuitively.
To understand const, convert your const to east const. West const is just an exception to the standard east const rule, where if there is no token in the type to the left it applies to the token on the right. The token on the right here is the entire type string& bundled into a type alias (type aliases are not macros). If instead you typed const string& the token on the right would be string and you'd get string const& in sane, east-const style.
East const is best const.

Why is std::is_const::value 'false' even though T's value_type is const?

#include <type_traits>
struct foo;
int main()
{
const foo *bar;
static_assert(std::is_const<decltype(*bar)>::value,
"expected const but this is non-const!");
}
This results in a failing static_assert which is unexpected. This is somewhat similar to this question on const references but not quite the same.
In my case, dereferencing bar should give an instance of const foo as its type but yet std::is_const is saying otherwise.
Shortly that's because a reference or a pointer to a const type is not a const type.
Note that decltype(*bar) isn't const foo, it's const foo & and they are really different beasts.
Consider the example given here:
std::cout << std::is_const<const int *>::value << '\n'; // false
std::cout << std::is_const<int * const>::value << '\n'; // true
We see that std::is_const<const int *>::value is false and std::is_const<int * const>::value is true.
That's because in const int * the type is pointer to something const, that is not a const type as intended by is_const (and the standard actually). In int * const the const qualifier applies to the pointer type and not to the pointed one, thus the type is a const one, no matter to what it points.
Something similar applies for const foo &, that is a reference to something const.
You can solve using this instead:
static_assert(std::is_const<std::remove_reference_t<decltype(*bar)>>::value, "expected const but this is non-const!");
Or even this, for you don't need to do *bar actually:
static_assert(std::is_const<std::remove_pointer_t<decltype(bar)>>::value, "expected const but this is non-const!");
In this case, by removing the pointer/reference with remove_pointer_t/remove_reference_t your type becomes const foo, that is actually a const type.
As a side note, the example above uses the C++14-ish std::remove_reference_t and std::remove_pointer_t type traits.
You can easily turn those lines of code to C++11 as it follows:
static_assert(std::is_const<typename std::remove_pointer<decltype(bar)>:: type>::value, "expected const but this is non-const!");
It's worth mentioning a few comments to the answer to give more details:
Thanks to #DanielFischer for the question:
Is there a short explanation why decltype(*bar) is const foo& rather than const foo?
I'm not a language-lawyer, but I guess it can be deduced from [expr.unary.op]/1 (emphasis mine):
The unary * operator performs indirection: the expression to which it is applied shall be a pointer to an object type, or a pointer to a function type and the result is an lvalue referring to the object or function to which the expression points.
And [dcl.type.simple]/4.4 (emphasis mine):
otherwise, if e is an lvalue, decltype(e) is T&, where T is the type of e;
Both referring to the working draft.
Thanks to #LightnessRacesInOrbit for the comment. Note that decltype(*bar) being const foo & is a funny C++ quirk of decltype, since *bar is not const foo &.

Why rvalue reference argument matches to const reference in overload resolution?

Potentially related articles:
Overload resolution between object, rvalue reference, const reference
std::begin and R-values
For a STL container C, std::begin(C) and similar access functions including std::data(C) (since C++17) are supposed to have the same behavior of C::begin() and the other corresponding C's methods. However, I am observing some interesting behaviors due to the details of overload resolution involving lvalue/rvalue references and constness.
DataType1 is int* as easily expected. Also, confirmed the by with Boost's type_id_with_cvr. const vector<int> gives int const* No surprise here.
using T = vector<int>;
using DataType1 = decltype(T().data()); // int*
using CT = const T;
using DataType2 = decltype(CT().data()); // int const*
using boost::typeindex::type_id_with_cvr;
cout << type_id_with_cvr<DataType1>() << endl; // prints int*
...
I tried std::data, which can also handle arrays and non-STL containers. But it yields int const*. Similarly, std::begin returns a const iterator, even though T is not const.
using T = vector<int>;
using DataType3 = decltype(std::data(T())); // int const* Why ???
using CT = const T;
using DataType4 = decltype(std::data(CT())); // int const*
Question: What is the reason of this difference? I expected that C.data() and std::data(C) would behave in the same manner.
Some my research: In order to get int* for DataType3, T must be converted to non-const lvalue reference type explicitly. I tried declval, and it was working.
using DataType3 = decltype(std::data(std::declval<T&>())); // int*
std::data provides two overloads:
template <class _Cont> constexpr
auto data(_Cont& __c) -> decltype(__c.data()) { return __c.data(); }
// non-const rvalue reference argument matches to this version.
template <class _Cont> constexpr
auto data(const _Cont& __c) -> decltype(__c.data()) { return __c.data(); }
While resolving overloaded functions for std::data, T(), which is non-const rvalue reference, is matched to the const T& version instead of T& version.
It was not easy to find this specific overload resolution rule in the standard (13.3, over.match). It'd be much clearer if someone could point the exact rules for this issue.
This behaviour is attributed to overload resolution rules. As per standard 8.5.3/p5.2 References [dcl.init.ref], rvalue references bind to const lvalue references. In this example:
std::data(T())
You provide to std::data an rvalue. Thus, due to overload resolution rules the overload:
template <class _Cont> constexpr
auto data(const _Cont& __c) -> decltype(__c.data()) { return __c.data(); }
is a better match. Consequently you get const int*
You can't bind a temporary to a non-const lvalue reference.
The only line that is mildly surprising is using DataType1 = decltype(T().data()); // int*.
...but it is still normal, member functions can be called on temporary objects without being treated as const. This is another non trivial difference between member functions and free functions.
For example, in C++98 (pre-rvalue refs) it was not possible to do std::ofstream("file.txt") << std::string("text") because operator<< is not member and the temporary is treated as const. If operator<< were a member of std::ofstream it would be possible (and may even make sense). (The situation changed later in C++11 with rvalue references but the point is still valid).
Here it is an example:
#include<iostream>
struct A{
void f() const{std::cout << "const member" << std::endl;}
void f(){std::cout << "non const member" << std::endl;}
};
void f(A const&){std::cout << "const function" << std::endl;}
void f(A&){std::cout << "non const function" << std::endl;}
int main(){
A().f(); // prints "non const member"
f(A()); // prints "const function"
}
The behavior is exposed when the object is temporarily constructed and used. In all other cases I can imagine, member f is equivalent to f free function.
(r-value reference qualifications && --for member and function-- can give you a more fine grained control but it was not part of the question.)

Why does a const reference to volatile int need a static_cast?

Given the following code:
struct Foo {
volatile int i;
};
const int& bar = foo.i;
I get:
error: invalid initialization of reference of type 'const int&' from expression of type 'volatile int'
Unless I provide a static_cast<volatile int>.
"Volatility" of types in C++ works in pretty much exactly the same way as "const-ness" -- that is, an object that is volatile cannot be assigned to a non-volatile reference, in just the same way that
const int i = 3;
int& j = i; // won't work
Similarly, only methods marked volatile can be called on a volatile object:
struct S
{
void do_something() volatile {}
void do_something_else() {}
};
volatile S s;
s.do_something(); // fine
s.do_something_else(); // won't work
Methods can be overloaded by "volatility", in the same way they can be overloaded by const-ness.
In C++ standardese, these things are known as cv-qualifiers, to emphasise that they work in exactly the same way. (C99 adds a third that works in the same way, restrict, available as an extension in some C++ compilers). You can change cv qualifiers using const_cast<> -- the more powerful static_cast<> isn't required.
EDIT:
Just to make clear, a type can have both const and volatile modifiers at the same time, giving a total of four possibilities:
int i;
const int j;
volatile int k;
const volatile int l;
As with const, a volatile object can only be referred to by a volatile reference. Otherwise, the compiler has no way of knowing that accesses to the object via the reference need to be volatile.
volatile const int& bar = foo.i;
^^^^^^^^
Unless I provide a static_cast<volatile int>
It's rarely a good idea to silence a compiler error by adding casts. This doesn't give you a reference to foo.i, but to a separate copy of it.
If you are doing something really strange that requires a non-volatile reference to a volatile object, you'd need const_cast to remove the qualifer:
const int& weird = const_cast<int&>(foo.i);

constant references with typedef and templates in c++

I heard the temporary objects can only be assigned to constant references.
But this code gives error
#include <iostream.h>
template<class t>
t const& check(){
return t(); //return a temporary object
}
int main(int argc, char** argv){
const int &resCheck = check<int>(); /* fine */
typedef int& ref;
const ref error = check<int>(); / *error */
return 0;
}
The error that is get is invalid initialization of reference of type 'int&' from expression of type 'const int'
This:
typedef int& ref;
const ref error;
Doesn't do what you think it does. Consider instead:
typedef int* pointer;
typedef const pointer const_pointer;
The type of const_pointer is int* const, not const int *. That is, when you say const T you're saying "make a type where T is immutable"; so in the previous example, the pointer (not the pointee) is made immutable.
References cannot be made const or volatile. This:
int& const x;
is meaningless, so adding cv-qualifiers to references has no effect.
Therefore, error has the type int&. You cannot assign a const int& to it.
There are other problems in your code. For example, this is certainly wrong:
template<class t>
t const& check()
{
return t(); //return a temporary object
}
What you're doing here is returning a reference to a temporary object which ends its lifetime when the function returns. That is, you get undefined behavior if you use it because there is no object at the referand. This is no better than:
template<class t>
t const& check()
{
T x = T();
return x; // return a local...bang you're dead
}
A better test would be:
template<class T>
T check()
{
return T();
}
The return value of the function is a temporary, so you can still test that you can indeed bind temporaries to constant references.
Your code gives error because the const qualifier in const ref error is just ignored because 8.3.2/1 says
Cv-qualified references are ill-formed except when the cv-qualifiers are introduced through the use of a typedef (7.1.3) or of a template type argument(14.3), in which case the cv-qualifiers are ignored.`
So error has type int& not const int& .
It's a very common mistake for English speaking people, because of the way the English grammar works.
I consider it extremely unfortunate that the C++ syntax would allow both:
const int // immutable int
int const // immutable int
to have the same meaning.
It doesn't make it easier, really, and isn't composable since:
const int* // mutable pointer to immutable int
int* const // immutable pointer to mutable int
certainly do NOT have the same meaning.
And this, unfortunately for you, what kicks in here, as #GMan explains.
If you wish to avoid this kind of error in the future, take the habit of qualifying your types (const and volatile) on their right, then you'll be able to treat a typedef as simple text replacement.
To maintain consistency with the Right Left Rule, I prefer to use 'cv' qualifiers like so.
int const x = 2; // x is a const int (by applying Right Left rule)
int const *p = &x; // p is a pinter to const int
In your example, I would write const ref error = check<int>(); like so
ref const error = check<int>(); // parsed as error is a const reference to an integer
As #Prasoon Saurav pointed out, cv qualifiers are ignored when introduced through typedef because as #GMan also says, that cv qualified references are ill-formed.
Therefore the declaration is effectively as below, which of course is an error.
int &error = check<int>();
Check out this for more information.
This is compiled:
typedef const int& ref;
ref error = check<int>();
VC++ compiler gives some explanation of your error: qualifier applied to reference type; ignored. Reference type must be declared as constant, const cannot be applied later.