can one conclude that the reference type of C++, when it was originally designed, was not well thought out?
What you conclude is ultimately your choice. However, I think calling C++ references "not well thought out" merely because they forbid a particular usage is wrongheaded. Tools exist to be used within the domain that they're intended for; that a tool may not work in every case doesn't mean that it's not working in 99.999% of them. And if it is working 99.999% of the time, that seems to be a tool that's doing its job.
After all, it's not like everyone has en-masse switched to std::reference_wrapper
for all of their reference needs. You don't see these things littering C++ codebases in lieu of language references. It is a specialized tool for specialized cases.
Indeed, the genesis of what became std::reference_wrapper
is boost::bind
. bind
is a tool that captures things and forwards them to a function, altering its signature. A user might want to capture some things as references, or they may want to capture some things as values. This requires tagging specific parameters to bind
with some syntax, and boost::reference_wrapper
is what makes that tagging work.
Even if language references behaved as you suggest, you would still need to tag arguments with specific syntax to get bind
to behave. After all, if you have a variable that is an object rather than a reference, you might still want to pass it as a reference to bind
. And that would require specific syntax.
That syntax might be language syntax rather than a library feature, but what exactly does that matter? C++ uses a lot of library things to handle stuff that other languages build-in (std::optional
, std::variant
, std::function
, etc). Indeed, some consider that to be a strength of the language, not a weakness.
The conceptual foundation of the C++ language reference is that a reference to an object is merely another name for that object. Given int i = <some_int>;
and int &ri = i;
, using the names i
and ri
should behave identically (type-deduction gymnastics aside). So long as the original object exists, using the name of a reference to that object has equivalent behavior to using the name of the object.
This foundational model explains all of the limitations of reference variables in C++. They cannot be rebound because then ri
would have different behavior from i
; you could not use them interchangeably. You cannot "copy a reference," because the syntax for copying one reference variable to another would look exactly like the syntax for copying the referenced object; again, you couldn't use them interchangeably.
And this model has real benefits.
First, it prevents errors. If you take a reference as a parameter, the meaning of that reference is permanently locked. Whatever object you were given is the object being referenced. Full-stop. So if you're using a reference parameter later in the function, you don't have to check to see if someone rebound it, either deliberately or by accident, because they can't do that. Even if the rebinding syntax were jab-you-in-the-eye obvious, not being able to rebind a reference means that you don't even have to think about that possibility.
Second, it makes it easier to optimize things. Because it is impossible for the reference to be rebound, the compiler doesn't have to look for reference rebinding code before doing optimizations around references. If the compiler can see which object a reference variable is bound to, it need look no further than that. This also benefits static analysis, since the tool doesn't have to look for reference rebinding.
Third and most important, it's simpler this way, and therefore easier to teach and use.
Under your suggested system, we would need a distinction between T const &
and T & const
. That is, whether the referenced object is const
or the reference itself is const
. Just look at how hard that is to understand with pointers, and you see the problem. Under the current system, that's not an important distinction; all references are "const".
Now consider the true horror: a reference to a reference.
In the current system, since a reference behaves identically to the object, the idea of a "reference to a reference" is nonsense. To have that, a reference variable would have to behave differently from the object it references. Since that's forbidden, we must forbid references to references. Generic code gets reference collapsing rules to make things easier there (so that if T
is a reference, you can still do T& t = <something>;
and get reasonable behavior).
Forbidding such things vastly simplifies reference usage. Consider how difficult pointers-to-pointers are to teach and use, and you have an inkling of the horror to be unleashed. Now consider that you could have pointers to references and vice-versa.
Do you want an int *&*&*&&
? Because what you're suggesting is how you get that.
There is further simplicity. Because the reference behaves identically to the object it references, there is no ambiguity in what a piece of language means. If I have some T &t
, and I see t = <something>;
, I never have to ask what that is doing. It is clearly assigning to the object being referenced because that's how references work.
If references are objects distinct from the referenced object (like reference_wrapper
), then what does t = <something>;
do? Let's say that we have the compiler determine whether an operation acts on the reference or the referenced object based on what that operation is. Well, assignment would either:
- Always rebind the reference. So if I want to assign to the object, I need special syntax.
- Always assign to the referenced object. So if I want to rebind the reference, I need special syntax.
- Pick which one to do based on
<something>
. If it's a reference, you rebind it and if it's an object, you assign.
Item 3 is ambiguous. I now need to inspect exactly what <something>
is to know what t = <something>;
actually means. Item 2 is the safest approach, but now you need to invent special syntax to say "rebind". That adds complexity.
Simple rules are easy to understand and follow. And there is great simplicity in "operations on references are operations on the object being referenced". Complicating that with an object that sometimes acts like the referenced object and sometimes acts like the reference? What are the rules for when it does one vs. the other? Can all users correctly remember those rules, on top of the thousands of other rules C++ requires you to know?
operator.
(operator dot), custom references (and references) could be implementable as a library.