15

I have the following code:

struct Foo {
    int var1;
    int var2;


    friend std::ostream& operator<<(std::ostream& os, const Foo& s){
        return os << "[Foo] " << s.var1 << "," <<  s.var2 ;
    }
};

int main() {
    Foo foo;
    foo.var1 = 1;
    foo.var2 = 2;
    std::list<Foo> list;
    list.push_back(foo);

    Foo &foo2 = list.front();
    foo2.var2 = 5;

    std::cout << "foo (" << &foo << "): " << foo << std::endl;
    std::cout << "foo2 (foo from list) (" << &list.front() << "): " << foo2 << std::endl;
}

I want both foo and foo2 to reference the same object. So when I assign 5 to foo2.var2, I would want to modify foo.var2 as well. Yet, as we can see in the following output this is not happening:

foo (0x7fffffffe140): [Foo] 1,2
foo2 (foo from list) (0x61ac30): [Foo] 1,5 

What would be the correct way to do that?

jscherman
  • 5,839
  • 14
  • 46
  • 88
  • Just to raise the warning... you'll have to make sure that your list will outlive all of the elements that you put into it, or you'll end up making dangling references. (Same issues as if you were storing pointers into the container) – Andre Kostur May 24 '17 at 22:44

4 Answers4

20

When you use push_back to insert elements into a list, push_back creates a copy which is inserted into the list. A solution is to use a std::reference_wrapper instead as the underlying type of the list, like

std::list<std::reference_wrapper<Foo>> lst;

and then push into it like

lst.push_back(foo);

Here is a super simple example that shows you how it works:

#include <functional>
#include <iostream>
#include <list>

int main() 
{
    int i = 42;
    std::list<std::reference_wrapper<int>> lst;

    lst.push_back(i);           // we add a "reference" into the list
    lst.front().get() = 10;     // we update the list
    std::cout << i;             // the initial i was modified!
}

Live on Coliru

You need the reference_wrapper since you cannot simply create a list of references, like std::list<Foo&>. Alternatively, you can use pointers, but I find the reference_wrapper approach more transparent.

In the simple example above note the need to use std::reference_wrapper::get() to obtain the underlying reference, as the reference_wrapper is on the left hand side of the assignment operator and hence won't be implicitly converted to int via [std::reference_wrapper::operator T&.

Below is your full working code modified to use reference_wrappers:

http://coliru.stacked-crooked.com/a/fb1fd67996d6e5e9

vsoftco
  • 55,410
  • 12
  • 139
  • 252
  • `std::reference_wrapper` *is* implicitly convertible to `T&`; however, I'm not sure if that would be considered in resolving an `operator=` overload, and even if so there would be a good chance of `reference_wrapper`'s copy constructor to make it ambiguous. The implicit conversion is more useful if passing to a function taking a reference. – Daniel Schepler May 24 '17 at 18:16
  • @DanielSchepler You're right, in fact the assignment operator will try the conversion on its right side, not on the left, so it won't work (the compiler will try to convert `10` to a `reference_wrapper`). – vsoftco May 24 '17 at 18:27
  • Which I think would resolve to the explicitly deleted constructor `reference_wrapper(T&&) = delete`, and therefore cause a compiler error. – Daniel Schepler May 24 '17 at 18:39
  • @DanielSchepler Exactly, was just playing around with it and can confirm. – vsoftco May 24 '17 at 18:41
4

Your std::list<Foo> contains actual Foo objects. When you call list.push_back(foo), it makes a copy of foo. You are then declaring foo2 to reference that copied object, not the original object. That is why foo is not updated when the members of foo2 are changed.

Try using pointers instead:

int main() {
    Foo foo;
    foo.var1 = 1;
    foo.var2 = 2;

    std::list<Foo*> list;
    list.push_back(&foo);

    Foo *foo2 = list.front();
    foo2->var2 = 5;

    std::cout << "foo (" << &foo << "): " << foo << std::endl;
    std::cout << "foo from list (" << foo2 << "): " << *foo2 << std::endl;
}
Remy Lebeau
  • 555,201
  • 31
  • 458
  • 770
3

For that you'll need reference semantics instead of value semantics. The most straightforward way to implement reference semantics is through pointers. Only some small changes in your example will do it:

struct Foo {
    int var1;
    int var2;

    friend std::ostream& operator<<(std::ostream& os, const Foo& s){
        return os << "[Foo] " << s.var1 << "," << s.var2 ;
    }
};

int main() {
    auto foo = std::make_unique<Foo>();
    
    foo->var1 = 1;
    foo->var2 = 2;

    // We want a non owning observer of foo
    auto& foo2 = *foo;

    std::list<std::unique_ptr<Foo>> list;
    
    // The ownership of foo is moved into the container
    list.emplace_back(std::move(foo));

    // Another non owning observer
    auto& foo3 = *list.front();
    foo3.var2 = 5;

    // foo is null here, since we moved it in the container, but we have foo2
    std::cout << "foo (" << &foo2 << "): " << foo2 << std::endl;

    std::cout << "foo from list (" << list.front().get() << "): " << foo3 << std::endl;
}
Guillaume Racicot
  • 39,621
  • 9
  • 77
  • 141
2

If you change your output to this:

std::cout << "foo " << foo << std::endl;
std::cout << "front " << list.front() << std::endl;
std::cout << "foo2 " << foo2 << std::endl;

You'll find the output looks like this:

foo [Foo] 1,2
front [Foo] 1,5
foo2 [Foo] 1,5

Indeed, when you got a reference, then set foo2, you did change the front of the list! The surprise here is that the front of the list is no longer equal to foo. When you push something into a list, it puts a copy in that list, not a reference.

Stewart
  • 4,356
  • 2
  • 27
  • 59