0

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!

aniliitb10
  • 1,259
  • 10
  • 14
  • 1) passing by `const &` does not mean that object is immutable, it means that reference has `const` qualifier which will effect overload resolution 2) you never modify its member, you modify a non-const-qualified object referenced by its member; note that even though you are accessing `_name` using `Person const &` it does not adjust type of `_name` to `std::string const &`. If `_name` was a pointer `std::string *` then `const` qualifier would be applied to pointer so it'll became `std::string * const`, but references are not rebindable by default so `std::string & const` does nothing. – user7860670 Sep 15 '19 at 09:18
  • ok, so perhaps, the fundamental questions are: 1) If I am accessing `_name` by `const PersonRef&` why its type is not adjusted to `const std::string&`. This is what I had expected. 2) what do you mean by _std::string & const does nothing_ ? if I declare `_name` to be `const std::string& `, this code doesn't compile as I can't ignore its `cost-ness` while binding it to `non-const lvalue reference` inside the function. – aniliitb10 Sep 15 '19 at 09:36
  • 1
    Accessing class members through a const-qualified reference does add const qualifier to class members. But `_name` member is a reference that is it has one level of indirection. Const qualifier is not propagated through that lever of indirection, it applies only directly to class member. That is accessing `_name` yields `std::string & const` and not `std::string const &` as you expect (also you seem be to confused by `const` placement) Since references are not rebindable `std::string & const` behaves exactly the same way as `std::string &`. – user7860670 Sep 15 '19 at 09:55
  • thanks, made a lot of sense! I was confused between `std::string & const` and `std::string const &` – aniliitb10 Sep 15 '19 at 10:02
  • 1
    [right-to-left / spiral reading order is indeed confusing](https://github.com/guaranteed-to-be-unique/Straight-Declarations/blob/master/Meme.png) – user7860670 Sep 15 '19 at 10:07

2 Answers2

1

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.

songyuanyao
  • 169,198
  • 16
  • 310
  • 405
0

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;}

};
HolyBlackCat
  • 78,603
  • 9
  • 131
  • 207
  • Ok, so from what I understood: non-const reference to `const T` should have made 1) its pointer const, 2) its non-reference member `const` but 3) it can't make its reference `const` as `references` are never const right? – aniliitb10 Sep 15 '19 at 09:42
  • 1
    @aniliitb10 References are not rebindable, that is **they are always const**, therefore const qualifier is omitted. – user7860670 Sep 15 '19 at 09:59
  • yes, thanks, I found a similar question which describes that the references are [inherently const](https://stackoverflow.com/questions/7420780/what-is-a-constant-reference-not-a-reference-to-a-constant). It made a lot of sense when I realized that const-ness of T is not applied recursively! – aniliitb10 Sep 15 '19 at 09:59