6

Besides the fact that all primitive types of C++ are copy assignable except the reference type, it also doesn't play well with containers or any other parts of the language where copy-assignable semantics are needed. I personally believe this is the reason that std::reference_wrapper had to be introduced to the language later to circumvent the limitations of it.

In retrospect, can one conclude that the reference type of C++, when it was originally designed, was not well thought out?
From a language design perspective, should it have been defined as something that behaves like the newly introduced reference wrappers?

1
  • 1
    i think that references are quirky, not for the typical reason people give (rebinding, redundancy w/pointers, life extensions, etc), those are mostly fine IMO, but for the reason that it is so difficult or impossible to implement at a 100% a custom reference-like objects (things that behave like references but for one or other reason can’t be simply written as “& of some value”, (e.g. not owning ranges). I might be missing some details but if the language provided operator. (operator dot), custom references (and references) could be implementable as a library.
    – alfC
    Apr 27, 2022 at 6:34

3 Answers 3

11

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:

  1. Always rebind the reference. So if I want to assign to the object, I need special syntax.
  2. Always assign to the referenced object. So if I want to rebind the reference, I need special syntax.
  3. 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?

10
  • well explained.
    – John Z. Li
    Oct 27, 2018 at 5:10
  • 5
    Actually, C++ already has rebindable references. They are called pointers. ;-) Oct 27, 2018 at 13:09
  • They are well thought out, true. But some reasons for it might not be true. Making it easy to teach and use is especially laughable, because you already have tons of rules in the language, and piling the new one not fully orthogonal to others will never make things easy, without the removal of the offending bloats.
    – FrankHB
    Dec 8, 2021 at 19:53
  • @FrankHB: C++ wasn't designed by a single person. A particular feature may have been designed by a person, but not the language as a whole. So some parts may be easy to teach by design, and other parts may not. That's not a reason to break the easy to teach parts. Dec 8, 2021 at 19:56
  • int *&*&*&& is bad for many reasons. Visibly, the horror comes directly from the declaration syntax (ironically, also shared by references). But more troubles are in the semantics: if references are allowed like this, they are just another awkward duplication of pointers. This is simply useless. And pointers are considered bad, mostly attributing to the misbehavior of unexpected properties in the values (but not the transformation of the values), e.g. unwanted null states, suspicious iterator operations, etc... All are misfeatures when user just want a reference to a single entity.
    – FrankHB
    Dec 8, 2021 at 19:58
7

The C++ reference type is not immediately problematic. A reference is not an object, but only an alias of an object. Keeping this in mind immediately solves a number of misunderstandings: a container contains objects, therefore not references. Whenever you need a reference-like behavior but also need an actual object, use a pointer or a wrapper.

It could be argued that references are unnecessary given pointers. However, comparing C code using pointers with C++ code using references will show that references drastically improve language ergonomics, at a slight cost of explicitness. References integrate well with C++'s concepts of object lifetime/RAII, with intermediate values (compare lvalue and rvalue references), and are created implicitly by function calls. The corresponding C code would feature a lot more &, *, and -> operators.

Note that C's pointer decay rules address similar language ergonomics concerns like C++ references. However, pointer decay only applies to arrays, string literals, and function pointers. In C++, references can be used to prevent decay.

Newer languages in the same space as C or C++ tend to make pointers and references more alike, or get rid of either.

  • Go only has pointers, but under some circumstances an lvalue is automatically converted to a pointer (e.g. for some method calls) and pointers might be dereferenced implicitly (there is no -> operator).
  • While Rust has pointers for unsafe code, it normally only uses references. These are more like C++ references with respect to ownership semantics, but are objects. You therefore sometimes have to dereference them explicitly.
  • Languages like Java or C# got rid of explicit references or pointers entirely, but this means objects no longer have value semantics. C# supports call-by-reference through additional parameter annotations (out-parameters or ref-parameters). But this only supports a fraction of the use cases of C++'s references. C# supports user-defined value types (structs), but makes it difficult to switch between value-semantics and reference-semantics like C++. C# supports pointers for unsafe code.
1
  • 1
    Worth noting that C# has added a lot more reference functionality lately. You can pass by ref/"const" ref, return by ref, and even distinguish between rebinding and assigning through. This was for the purpose of controlling by-val and by-ref for value types, like you mention.
    – chris
    Oct 27, 2018 at 2:50
3

C++ does not define a category primitive types.

You may be mistaking fundamental types, which is a category of void, std::nullptr_t and the various arithmetic types. These are assignable (except void).

Reference types are compound types, the same level of categorisation as pointer, pointer-to-member, array, function, enumeration, and class types.

In common with arrays1, functions and various classes they are not assignable.

  1. arrays are the real odd ones out. The other non-assignable types are either not objects, or had assignment taken away by the author.
1
  • 1
    The odd ones come from C (together with the infamous array decaying). C++ has std::array as a saner replacement. Given the replacement, I don't think it makes much more troubles than the oddity of void not being a real unit type in generic code.
    – FrankHB
    Dec 8, 2021 at 19:30

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.