0

I want an object which can wrap a value symantic type and pretend that it is a reference.

Something like this:

int a = 20;

std::list<Wrap<int>> l1;
l1.push_back(a);

std::list<Wrap<int>> l2;
l2.push_back(a);

l2.front() = 10;

cout << l1.front() << endl; // output should be 10

While writing this question it occured to me that a shared_ptr might be what I want. However I am not sure if a pointer symantic object is what I am looking for. Perhaps there is no alternative in the standard library?

std::shared_ptr<int> a = std::make_shared(10);

std::list<std::shared_ptr<int>> l1;
l1.push_back(a);

std::list<std::shared_ptr<int>> l2;
l2.push_back(a);

*(l2.front()) = 20; // not exactly what I wanted to write

cout << *l1.front() << endl; // prints 10

I found std::reference_wrapper but this appears to be the opposite of what I want. It appears to permit a reference type to be used like a value type which is the reverse of what I wanted.

Any thoughts?

janekb04
  • 4,304
  • 2
  • 20
  • 51
FreelanceConsultant
  • 13,167
  • 27
  • 115
  • 225
  • Use `std::list`. Then `push_back(&a)` in your example, and do assignments after dereferencing e.g. `*(ls.front()) = 10;`. `shared_ptr` could be used, except that it manages lifetime of the pointed-to objects (which is not a good idea in your case, given that `a` has automatic storage duration). – Peter Aug 28 '22 at 11:52
  • Not sure what you hope to gain by *pretending* an lvalue is a reference. Pushing a value to several different containers will make several different copies. How would you expect changing one to change the others? – Adrian Mole Aug 28 '22 at 11:53
  • @AdrianMole That is exactly the point! If one value is changed, all references should show that the value has changed! – FreelanceConsultant Aug 28 '22 at 11:54
  • 2
    Well, then you aren't *pretending* they're references, are you. They ***are*** references. Make your containers contain references and use the `std::reference_wrapper` to allow implementation. – Adrian Mole Aug 28 '22 at 11:55
  • @AdrianMole Imagine a matrix. I want to be able to store all the elements in a list, and then have lists for columns and rows... Of course if an object is changed using row object, it should change everywhere else also. This isn't exactly my application, but it is close enough to illustrate the point. – FreelanceConsultant Aug 28 '22 at 11:56
  • @AdrianMole Indeed! – FreelanceConsultant Aug 28 '22 at 11:56
  • @Peter Can you explain the problem you foree in a bit more detail? Does it matter if `a` goes out of scope, but the lists still exist? Surely the reference count of the shared pointer will decrease from 3 to 2? – FreelanceConsultant Aug 28 '22 at 11:57
  • If `a` goes out of scope but the list (or a copy of the list) exists - with a reference or pointer to `a`, then any use of that element in the list gives undefined behaviour. If you wrap the address of `a` in a `shared_ptr`, the same thing happens - with the additional kicker that, by default, `shared_ptr` will attempt to `delete` that pointer - which also causes undefined behaviour. You seem to believe that holding a reference to something keeps it alive - which is untrue in C++ (except in very specific situations). – Peter Aug 28 '22 at 12:00
  • In the second code block, `cout << *l1.front() << endl; // prints 10` did not print **10** for me. – Eljay Aug 28 '22 at 12:22
  • Are you looking for [std::reference_wrapper](https://en.cppreference.com/w/cpp/utility/functional/reference_wrapper)? – Jesper Juhl Aug 28 '22 at 12:23
  • @JesperJuhl Is that not the reverse of what I want, for the reasons mentioned in the question? – FreelanceConsultant Aug 28 '22 at 12:42
  • 5
    No, `std::refrence_wrapper` seems to be exactly what you want, I'm sure if you try it it'll work – Alan Birtles Aug 28 '22 at 12:43
  • @AlanBirtles Ok, it does indeed seem to work. I guess I must have missunderstood the documentation page on it. The question is why does that work, if a `reference_wrapper` stores a value type and is copyable? – FreelanceConsultant Aug 28 '22 at 12:51
  • It seems to me like the problem is not how to put a reference in a list, it's how to make it behave like a value type. The short answer is that you can't do that generically in C++ because the way the language works. You will always need something like a `value_or_reference` class in there, like `std::list>`. This is different in some other languages like C# or Java where type boxing or nullable types is a thing, but it comes at the price of one of C++'s most valuable promises: You don't pay for what you don't use. – Fozi Aug 28 '22 at 12:56
  • `reference_wrapper` is basically just a copyable reference, it allows storing a reference in a container which would normally only be able to store a value type. – Alan Birtles Aug 28 '22 at 13:01

1 Answers1

3

Proxy Object

As far as I know, there isn’t such a wrapper in the Standard Library. Fortunately though, you can implement one yourself rather easily.

How to start? Well, we’d like our Wrap<T> to be possibly indistinguishable from a T&. To achieve this, we can implement it as a proxy object. So, we’ll be making a class template storing a T& ref and implement operations one by one:

template<typename T>
struct Wrap
{
private:
    T& ref;
public:
    // ???
};

Construction

Let’s start with constructors. If we want Wrap<T> to behave like a T& it should be constructible from the same things that T& is constructible. Well then, what is a reference x constructible from?

A non-const lvalue reference like T& x = ...; needs to be constructed from an lvalue of type T. This means that it can be constructed from:

  • an object of type T
  • an object of a type derived from T
  • a different lvalue reference to T
  • an object convertible to T&
  • a function call returning T&

A const lvalue reference like const T& x = ...; can also be constructed from a braced-initializer-list or materialized from a temporary, however if we wanted to implement that, then we’d need to actually store a T inside of our class. As such, let’s focus on a non-const reference.

First, let’s implement a constructor from T& which will cover all of the cases shown above:

constexpr Wrap(T& t) noexcept :
    ref{t}
{
}

The constructor is constexpr and noexcept because it can be. It is not explicit because we want to be able to use Wrap as transparently as possible.

We need to remember though, that a reference needs to be initializable from a different reference. Because we want our type to behave just like a builtin reference, we need to be able to

  • initialize a Wrap from a T& (already implemented above)
  • initialize a Wrap from a different Wrap
  • initialize a T& from a Wrap

To meet these criteria, we’ll need to implement a conversion operator to T&. If Wrap<T> will be implicitly convertible to T&, then assigning it to a builtin reference will work.

Actually, this:

int x;
Wrap w = x;
Wrap w2 = w;

will also work (ie. w2 will be Wrap<int> rather than a Wrap<Wrap<int>>) because the conversion operator takes precedence.

The implementation of the conversion operator looks like this:

constexpr operator T&() const noexcept
{
    return ref;
}

Note that it is constexpr, noexcept and const, but not explicit (just like the constructor).

Miscelaneous operations

Now that we can construct our custom reference wrapper, we’d also like to be able to use it. The natural question is “what can you do with a reference”? Well, builtin references aren’t objects but merely aliases to existing objects. This means that taking the address of the reference actually returns the address of the referent or that you can access members through a reference.

All these things could be implemented, for example by overloading the arrow operator operator-> or the address-of operator operator&. For simplicity though, I will omit implementing these operations and focus only on the most important one: assignment.

Assignment

Firstly, we’d like to be able to assign a Wrap<T> to a T& or a T or basically anything that can be assigned a T. Fortunately, we already got that covered by having implemented the conversion operator.

Now we only need to implement assignment to Wrap<T>. We could be tempted to just write the operator like this:

constexpr T& operator=(const T& t)
{
    ref = t;
    return ref;
}

constexpr T& operator=(T&& t) noexcept
{
    ref = std::move(t);
    return ref;
}

Seems fine, right? We have a copy assignment operator and a noexcept move assignment operator. We return a reference as per custom.

Well, the problem is that this implementation is incomplete. The thing is that we don’t check

  • is T copy-assignable
    • if it is, then is it nothrow-copy-assignable
  • is T move-assignable
    • if it is, then is it nothrow-move-assignable
  • what is the return type of T’s assignment operator (it could be the customary T&, but it could also theoretically be anything else, like void)
  • does T have any other assignment operators

This is a lot of cases to cover. Fortunately, we can solve this all by making our assignment operator a template.

Let’s say that the operator will take an object of arbitrary type U as its argument. This will cover both the copy and move assignment operators and any other potential assignment operators. Then, let’s say that the return type of the function will be auto, to let the compiler deduce it. This gives us the following implementation:

template <typename U>
constexpr auto operator=(U u)
{
    return (ref = u);
}

Unfortunately though, this implementation is still not complete.

  • We don’t know if the assignment is noexcept
  • We don’t distinguish copy assignment and move assignemnt and could potentially be making unnecessary copies.
  • Is the return type (auto) really correct?

To solve the first issue we can use a conditional noexcept. To check if the assignment operator is noexcept we can either use the type trait std::is_nothrow_assignable_v or the noexcept operator. I think that using the noexcept operator is both shorter and less error-prone, so let’s use that:

template <typename U>
constexpr auto operator=(U u) noexcept(noexcept(ref = u))
{
    return (ref = u);
}

To solve the issue of distinguishing copies and moves, instead of taking a U u, we can take a forwarding reference U&& u, to let the compiler deal with all of this. We also need to remember about using std::forward:

template <typename U>
constexpr auto operator=(U&& u) noexcept(noexcept(ref = std::forward<U>(u)))
{
    return (ref = std::forward<U>(u));
}

There is a bit of code duplication, but, unfortunately, it is inevitable, unless we’d use std::is_nothrow_assignable_v instead.

Finally, is the return type correct? Well, no. Because C++ is C++, parentheses around the returned value actually change its type (ie. return(x); is different from return x;). To return the correct type, we’ll actually also need to apply perfect forwarding to the returned type as well. We can do this by either using a trailing return type or a decltype(auto) return type. I will use decltype(auto) as it’s shorter and avoids duplicating the function body yet again:

template <typename U>
constexpr decltype(auto) operator=(U&& u) noexcept(noexcept(ref = std::forward<U>(u)))
{
    return ref = std::forward<U>(u);
}

Conclusion

Now, finally, we have a complete implementation. To sum things up, here it is all together (godbolt):

template<typename T>
struct Wrap
{
private:
    T& ref;
public:
    constexpr Wrap(T& t) noexcept :
        ref{t}
    {
    }
    constexpr operator T&() const noexcept
    {
        return ref;
    }

    template <typename U>
    constexpr decltype(auto) operator=(U&& u) noexcept(noexcept(ref = std::forward<U>(u)))
    {
        return ref = std::forward<U>(u);
    }
};

That was quite a bit of C++ type theory to get through to write these 21 lines. Oh, by the way, did I mention value categories...

janekb04
  • 4,304
  • 2
  • 20
  • 51
  • 1
    This is almost exactly the same as `std::refrence_wrapper` other than it doesn't work properly, you can't change what it is pointing to so it may have unexpected behaviour inside a `std::vector`. `ref` would be better stored as a pointer then you can change the object it points to (and then it would just be `std::refrence_wrapper`) – Alan Birtles Aug 28 '22 at 12:42
  • @AlanBirtles I know that I can’t change what it’s pointed to - that’s the point. Instead the assignment operator can modify the value of the current pointed-to object. – janekb04 Aug 28 '22 at 12:45
  • 2
    This is what, as I understand, OP is asking for. They don’t want to have a wrapper which acts as a rebindable reference, but a wrapper which most closely mimics the language level references - including assignment, like shown in the example in the question. – janekb04 Aug 28 '22 at 12:48
  • That's correct actually. Assignment should change the value of the object, not change what the reference "points to". – FreelanceConsultant Aug 28 '22 at 12:53
  • All I would say is that it should perhaps have a `Wrap *clone()` function instead of `Wrap(const Wrap& other)`, since not having the same behaviour between clone construction and `operator=` is perhaps a bit misleading to the client. – FreelanceConsultant Aug 28 '22 at 13:01
  • Can you explain what `noexcept(noexcept(other))` does? I'm trying to get this to compile but I don't understand this particular function at the moment, so I don't know how to fix my compiler error. – FreelanceConsultant Aug 28 '22 at 13:06
  • Also how does `decltype(auto)` work ? To me that seems bizzare although I can guess at what it is doing. – FreelanceConsultant Aug 28 '22 at 13:09
  • @FreelanceConsultant I provided a copy constructor so `Wrap` can be used inside containers which move their contents, like `std::vector`. This makes the type work transparently like a reference. Admittedly though, I forgot to provide an additional `operator=` analogous to the copy constructor. I will edit the answer to include it. **EDIT:** Actually, no additional assignment operator is needed thanks to an implicit conversion occurring. – janekb04 Aug 28 '22 at 13:10
  • Ah yes of course that would be needed as well. How does that function work though? I am absolutly mistified by the syntax. – FreelanceConsultant Aug 28 '22 at 13:15
  • 1
    @FreelanceConsultant I will explain `noexcept(noexcept())` and `decltype(auto)` in the answer _soon_. BTW, to compile this you’ll need at least C++17. – janekb04 Aug 28 '22 at 13:15
  • @janekb04 Thanks, this is quite extensive. Is it true that, generally speaking, many of these things can be made `constexpr`? That doesn't sound right to me. Let's assume that putting `constexpr` everywhere here is correct - does it work because we are dealing with a reference? In my mind, I was thinking - how can these constructors be `constexpr` if I am working with objects, which in general, have their values determined at runtime, for example, from reading files, user input, etc... – FreelanceConsultant Aug 28 '22 at 15:36
  • @FreelanceConsultant My rule of thumb is to make literally everything `constexpr`. If it won’t work, then the compiler will complain. In this particular case this can be `constexpr` because indeed, most operations on references are constexpr. The standard says so because in practice references are usually implemented in terms of pointers. In most cases, this constexpr won’t make a difference, but if you’d be writing some compile time code, then having constexpr on Wrap could be useful. Constexpr means potentially compile time. Only consteval means, certainly compile time. – janekb04 Aug 28 '22 at 15:47
  • @janekb04 Ok interesting - why not make everything `consteval` ? Sorry if that's a dumb question, that really isn't something I know much about. – FreelanceConsultant Aug 28 '22 at 15:57
  • Btw, I'm nearly there with this but I obtain this error: It could be that my *compiler* has an issue. `error: 'ref' was not declared in this scope` `constexpr decltype(auto) operator=(U&& other) noexcept(noexcept(ref= std::forward(other)))` Any thoughts? I'm using `mingw`. Not sure exactly which version, but I can potentially find out. – FreelanceConsultant Aug 28 '22 at 16:15
  • @FreelanceConsultant `constexpr` means: _this function can be invoked at compile time and used in compile time computations; however, it can also be used by runtime code_. `consteval` means: _this function must only be invoked at compile time; you cannot use it at runtime_. As such, you cannot make everything `consteval` as that would require your entire program to be executed at compile time. – janekb04 Aug 28 '22 at 16:16
  • I've managed to figure out that error. I had my member variable `ref` declared at the end of the class, rather than at the top. Why does the compiler complain if the `private` member variable section is placed at the end of the class? That seems a bit odd? Compiler bug perhaps? – FreelanceConsultant Aug 28 '22 at 16:21
  • 1
    @FreelanceConsultant It’s a compiler bug. Newest gcc compiles this code: https://godbolt.org/z/c4zET8fje . It seems to have been fixed in version 12.1, released in May 2022. – janekb04 Aug 28 '22 at 16:22