Mutable Lvalue Reference of Parameter Passed by const Reference - c++

I just found an interesting case: I passed an object by const reference and still I was able to modify its member (which happens to be an lvalue reference). Following is an example:
#include <iostream>
#include <string>
struct PersonRef
{
PersonRef(std::string& name_) : _name(name_) {}
std::string& _name; // <==== IMPORTANT: it is a reference
};
void testRef(const PersonRef& pr) {
std::string& name = pr._name; // binding to lvalue reference? How does it even compile?
name = "changed!";
}
int main() {
std::string name = "trivial_name";
PersonRef pr{name};
std::cout << pr._name << "\n"; // prints: trivial_name
testRef(pr);
std::cout << pr._name << "\n"; // prints: changed!
}
I used to think that if the parameter is passed by const ref, then object is immutable but this doesn't appear to be the case here. Could someone please explain this? Thanks!

Note that in const member function, the data member _name itself will be considered as const. This doesn't make any difference on _name because it's a reference, which can't be const-qualified. (In a sence the reference is always const, you can't modify the reference itself, the reference can't be rebound to other object after initialization.)
On the other hand, the referenced object won't be become const, so it's still possible to be modified, _name won't become const std::string& (reference to const).
Similar thing happens on pointer members; in const member function they become const pointers, but not pointers to const; you still could modify the pointed objects if they're non-consts from the beginning.

Consider a different example.
Let's say you have struct A {int *x;};.
When accessed through const A &ref, x would have type int *const - a const pointer to (non-const) int.
As you can see, const is not added recursively to the pointer. It's only added at the top level.
Back to your code. Strictly speaking, const PersonRef& pr is not a const reference. It's a non-const reference to const PersonRef.
(In theory, a const reference would be written as PersonRef &const pr. But since references are not rebindable to begin with, adding const wouldn't do anything, and thus isn't allowed. So technically references are never const, even though you can't rebind them.)
The compiler can't add const-ness to to std::string& _name, since references can't be const. And it doesn't add const-ness recursively to the referenced type, simply because that's how the language works.
If you don't want this behavior, make std::string& _name private and add a pair of const and non-const accesors:
class PersonRef
{
std::string &_name;
public:
PersonRef(std::string& name_) : _name(name_) {}
std::string &name() {return _name;}
const std::string &name() const {return _name;}
};

Related

Why is const redundant when using const functions C++?

So all modifiers that I have come across in C++ have came prior to the name of the function. Why is const different? Why must it both precede the name and come prior to the function block?
const int getSize() const;
or
const int getSize const {
...
}
These are 2 different usages of the const keyword.
In this case:
const int getSize() {
the function is returning an int that is const, i.e. the return value cannot be modified. This is not very useful, since the const in the return value is going to be ignored (compilers will warn about this). const in the return type is only useful when returning a const*, or a const&.
In this case:
int getSize() const {
this is a const-qualified member function, i.e. this member function can be called on const objects. Also, this guarantees that the object will not be modified, even if it's non-const.
Of course, you can use both of these together:
const int getSize() const {
which is a const-qualified member function that returns a const int.
The const before the function is applied to the return type of the function. The const after is only for member functions and means that the member function is callable on a const object.
As a note returning a const int does not make sense, many compilers will warn that the const gets discarded.

C++ const correctness with reference members

I have a fstream & member in a class, on which I'm calling the seekg function in a const function of the class, and yet the code compiles. I checked, and the seekg is not declared const (nor should it be), so how is this happening?
This is my code:
class Test {
fstream &f;
public:
Test(fstream &f_): f(f_) {}
int fid() const {
f.seekg(5);
return 0;
}
};
It turns out the const does not apply to members that are pointers or references, as stated here.
The best explanation I've seen of this is here where it states that inside const member functions, this is a const T *, where T is the class.
In your example that means that all the const modifier on fid() does is to change this from a Test * to a const Test * inside the function. When you write f., this is accessed as this->f. which is of type fstream & const. The reference is const but what it refers to is not, so calling a function that modifies it causes no problems.
The rule is defined in [expr.ref]/4:
If E2 is declared to have type “reference to T”, then E1.E2 is an lvalue; the type of E1.E2 is T. [...]
In practice you should consider a reference to T, as a const pointer to T with automatic dereferencement. Internaly this is what are reference. And inside the standard, all rules that applies to reference (see [basic.life] for example) are those rules that would apply to a const pointer:
class Test {
fstream * const f;
public:
Test(fstream &f_): f(&f_) {}
int fid() const {
f->seekg(5);
return 0;
}
};

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.)

Can I have a const member function that returns *this and works with non-constant objects?

If I understand correctly, a member function that is not supposed to modify the object should be declared as const to let the users know about the guarantee. Now, what happens when that member function returns the reference to *this? For example:
class C{
public:
C &f() const {return *this;}
};
Inside C::f(), this has the type const C*, so, the following will not compile:
int main() {
C c; // non-constant object!
c.f();
return 0;
}
Of course, we could provide a non-const version of C::f() to work on non-constant objects:
class C{
public:
const C &f() const;
C &f();
};
However, I do not believe that this is what we want. Note that the non-constant version does not change the object, but this promise to the users is not expressed... Am I missing something?
EDIT: Let me just summarize the question for clarity: f() does not modify the object on which it is called, so declaring it as C &f(); without making it a const member is misleading. On the other hand, I do want to be able to call f() on non-const objects. How do I resolve this situation?
EDIT: It comes out from all the discussion that took place in the comments that the question was based on an incorrect understanding of what constness of a function member implies. The correct understanding that I am taking away for myself is:
A member function that returns a non-const reference is intended to allow its users to change the object through the returned reference. Therefore, even though this function does not change the object by itself, there should be no inclination to declare it to be a const member!
Your problem is the original function definition:
C &f() const {return *this;}
here you return a non-const reference to the const object, which would allow changing the const object and would be dangerous, therefore it's forbidden.
If you were to write it as
const C &f() const {return *this;}
would be callable from both const and non-const objects and would always return a const reference.
Of course, we could provide a non-const version of C::f() to work on non-constant objects. However, I do not believe that this is what we want.
Probably that is exactly what you want. It's the only way to return a non-const reference when called on non-const objects and keep const correctness when calling it on const objects.

Int& to const int -static vs dynamic-

class A
{
public:
A(int i = 25) {x = i;y=new int[i];for(int j=0;j<i;j++) y[j]=j;}
int& f() const {return x;}
int& operator[](int i) const {return y[i];}
private:
int x,*y;
};
int main()
{
A a(15);
cout << a[5];
cout << a.f();
return 0;
}
When I'm trying to compile the code it says
"Invalid initialization of reference of type int& from expression of type const int"
regarding f() function.
I understand it,since it returns a non-const reference to something declared as const by the function. But shouldn't it behave the same with the [] overloading?
It also returns a non-const reference to something that the function declared as const,but it shows no error there.
What's the difference?
It is telling you that you can't return a non-const lvalue reference to a data member from a const member function. You need
const int& f() const {return x;}
You may decide to provide a non-const overload if needed:
int& f() {return x;}
As for operator[], it does not return a reference to a data member. You cannot modify x or y via operator[], so it is really a const member function. You may decide to disallow modification to the data pointed at by y, and this would make sense if your class models an array. But it isn't strictly necessary and the compiler has no reason to enforce it.
const int& operator[](int i) const {return y[i];}
int& operator[](int i) {return y[i];}
The issue is here:
int& f() const {return x;}
Your function is marked const, so it can be only invoked by const instances. However, you return a non-const reference, therefore if valid you can use it an modify const instances. The compiler is not happy with this. Hence f() should return const int& instead.
On the other hand, int& operator[](int) const compiles, since you return a reference to the data the pointer member y points to, but you cannot modify the pointer itself. In other words, on a const instance, the pointer y is const, i.e. int * const y, but not the data. Therefore bitwise const-ness is preserved, but of course logical const-ness is not, however the compiler only cares about bit-wise const-ness.
To enforce logical const correctness, one option is to write 2 versions of your operator[]:
const int& operator[](int i) const {return y[i];}
and
int& operator[](int i) {return y[i];}
Note that the second version should be marked non-const, as otherwise you'd try to overload two functions that differ only by their return type. If you want to avoid code duplication in the non-const version, you can make use of the const version via a const_cast, like
int& operator[](int i)
{
return const_cast<int&>(const_cast<const A&>(*this)[i]); // use the const version
}
EDIT
There is a proposal to introduce a const-propagating wrapper for pointer-like data members, propagate_const, which in effect will also make the data pointed to const, see
http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2014/n4057.pdf
In C++ language constness of a class implies immediate constness of all its members (except for mutable ones). However, constness of the class does not propagate to data pointed by pointer members or referenced by reference members. That pointed/referenced data is not part of the class and its constness is not affected by constness of the class.
In your example, declaring a member function as const makes it treat members x and y as const. However, the data pointed by y does not become const, e.g. neither *y nor y[i] are const.
Propagation of constness from the enclosing class to the pointed/referenced data is actually a part of user intent. It is something you need or don't need depending on what you are trying to implement. The language leaves it completely up to you (or to a library-level solution).