0

Typically, I use references for their non-nullability over pointers. However, I've run into issues with wanting to store references in vectors, or wanting a non-null field in an object that can be reassigned, except since references aren't re-assignable, I have to use pointers instead.

Why is this a feature of references? I can't see what benefits this brings, and there are plenty of cases where I just want a non-null pointer with no other baggage. Is there some ambiguous case or some compiler limitations that justifies why the language is like this?

k huang
  • 409
  • 3
  • 10
  • 3
    Because that's how the language designed it. If you want to assign again, use a pointer. If references could be assigned, that would allow programmers to create dangerous code. Basically, a reference is most likely implemented with pointers so you can, with a bit of weird tricks, assign it again. But then, why? – Michael Chourdakis Jul 13 '22 at 21:02
  • 2
    Or use a [`std::reference_wrapper`](https://en.cppreference.com/w/cpp/utility/functional/reference_wrapper) – Ted Lyngmo Jul 13 '22 at 21:03
  • 4
    a reference is just an alias to another object. It doesn't make a lot of sense to have an alias that changes what aliases to. – NathanOliver Jul 13 '22 at 21:06
  • *I can't see what benefits this brings* -- `void func(Widget& foo) { }` -- I want any Widget that is passed to be valid. No nulls allowed. Unlike other languages that let you pass `null` or similar, C++ enforces this at compile time, i.e. there is no need for `func` to write any code checking for a "null" reference, quite simply, because a null reference does not exist in a valid C++ program. – PaulMcKenzie Jul 13 '22 at 21:09
  • "I just want a non-null pointer with no other baggage" -> `template using nonnullptr = T*;` There you go, self-documenting non-null, reassignable gizmo. Go one step farther and make a wrapper class if you want to prevent accidental mixing of null and non-null gizmos. – Ben Voigt Jul 13 '22 at 21:16
  • @PaulMcKenzie: C++ does NOT enforce reference validity at compile-time, nor can even the best static analyzers detect 100% of potential dangling reference bugs. Languages that do guarantee reference validity at compile-time do so with the help of some extreme restrictions on reference lifetime (for example C# `return ref`) – Ben Voigt Jul 13 '22 at 21:19
  • 2
    @NathanOliver: No, a reference names a storage location, just like a pointer does. That's what allows it to access a new object created in-place in the same storage, as well as storing a reference to an object that hasn't finished construction yet. https://eel.is/c++draft/basic.life#8 – Ben Voigt Jul 13 '22 at 21:22
  • 3
    References are meant to be pretty opaque as far as the type system goes, and really serve no greater purpose than be another name for some other object. They aren't meant to be thought of as an intermediary to an object, they *are* that object by some name. You can't re-purpose identifiers to denote some other object in a given scope, so you can't rebind a reference. – StoryTeller - Unslander Monica Jul 13 '22 at 21:24
  • Now, I'm not suggesting that your functions write code to check whether their reference parameters are valid; there's nothing that can be done safely with an invalid reference, neither "check if valid" or any other operation. But it simply is not true that a reference is guaranteed valid by type checking. – Ben Voigt Jul 13 '22 at 21:25
  • 2
    The decision to make references non-rebindable is simply an additional feature of references. It makes it easier to reason about what code does. When you read code, you know a certain reference always refers to the same thing and you can assume that it refers to something. With pointers, you don't have these assumptions, making it somewhat harder to reason about code. – François Andrieux Jul 13 '22 at 21:33
  • 1
    @BenVoigt That passage just requires that a reference to an object refers to the new object when a new object is constructed over the storage the old object used. It may be convenient to implement this by making references work as pointers, pointing to storage. But that is an implementation detail. I don't see how your interpretation is more correct than NathanOliver's to warrant a correction. If anything https://eel.is/c++draft/dcl.ref#5 seems to favor the notion that references refer to objects and functions, and not storage. – François Andrieux Jul 13 '22 at 22:11
  • That's the way the language is designed. A reference, for as long as it exists, is an alias for a particular object. The language also provides pointers, which are assignable (can change what a pointer points at) and nullable. The properties of a reference make it easier to reason about what code does (if code uses a reference, the object it refers to cannot change) and that allows a range of optimisation opportunities to the compiler that pointers do not. – Peter Jul 13 '22 at 22:56
  • _"Why is this a feature of references?"_ Because the compiler can do many optimizations with the assurance that a reference is simply an alias. – Drew Dormann Jul 13 '22 at 23:06
  • References can be rebound but only if they are inside a class and you are using c++20 or higher. Even then you have to write copy and assignment constructors. The reason is that references are nameless objects that can only be identified when inside a class. What appears to be their name always refers to something else. – doug Jul 14 '22 at 02:50

1 Answers1

1

The duplicates listed go into detail why you can't reassign a reference. But they only apply to a stand alone reference, not one in a class.

In c++20 you can reassign references (and consts) if they are a class member but you have to write a copy assignment. c++20 has changed the rules from prior versions where this was impossible.

Here's a simple example:

#include <vector>
#include <memory>
#include <algorithm>

struct A {
    int& ri;
    A(int& ri) :ri(ri) {};
    A& operator=(const A& rhs) {
        std::destroy_at(this);  // only needed if class is not trivially destructable
        std::construct_at(this, rhs);
        return *this;
    }
};

int main()
{
    int i0{ 6 }, i1{ 5 };
    std::vector<A> v;
    v.emplace_back(A{ i0 });
    v.emplace_back(A{ i1 });
    std::sort(v.begin(), v.end(), [](const A& a, const A& b) {return a.ri < b.ri; });
    std::cout << v[0].ri << '\n';
    std::cout << v[1].ri << '\n';
    v[0] = v[1];    // references are reassigned
    std::cout << v[0].ri << '\n';
    std::cout << v[1].ri << '\n';
}
doug
  • 3,840
  • 1
  • 14
  • 18
  • Similar to your answer [here](https://stackoverflow.com/a/72693416/12002570) – Jason Jul 14 '22 at 04:11
  • @AnoopRana Yep, but rebinding R value references are a can of worms, while interesting, probably best avoided. – doug Jul 14 '22 at 04:15