Как управлять элементами std:: list в качестве ссылок?

У меня есть следующий код:

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;
}

Я хочу, чтобы оба foo и foo2 ссылались на один и тот же объект. Поэтому, когда я назначаю 5 в foo2.var2, я хотел бы также изменить foo.var2. Однако, как видно из следующего вывода, этого не происходит:

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

Каким будет правильный способ?

Ответ 1

Когда вы используете push_back для вставки элементов в список, push_back создает копию, которая вставляется в список. Решением является использование std::reference_wrapper вместо этого в качестве базового типа списка, например

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

а затем нажмите на него, как

lst.push_back(foo);

Вот супер простой пример, который показывает вам, как он работает:

#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

Вам нужно reference_wrapper, так как вы не можете просто создать список ссылок, например std::list<Foo&>. Кроме того, вы можете использовать указатели, но я нахожу подход reference_wrapper более прозрачным.

В простом примере выше обратите внимание на необходимость использования std::reference_wrapper::get() для получения базовой ссылки, так как reference_wrapper находится слева с правой стороны оператора присваивания и, следовательно, не будет неявно преобразован в int через [std::reference_wrapper::operator T&.

Ниже приведен ваш полный рабочий код, измененный для использования reference_wrapper s:

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

Ответ 2

Ваш std::list<Foo> содержит фактические объекты Foo. Когда вы вызываете list.push_back(foo), он делает копию Foo. Затем вы объявляете foo2 ссылкой на этот скопированный объект, а не на исходный объект. Вот почему Foo не обновляется при изменении элементов foo2.

Попробуйте вместо этого использовать указатели:

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;
}

Ответ 3

Если вы измените свой вывод на это:

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

Вы увидите, что результат выглядит следующим образом:

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

В самом деле, когда вы получили ссылку, затем установите foo2, вы изменили переднюю часть списка! Удивительно, что фронт списка больше не равен foo. Когда вы вставляете что-то в список, он помещает копию в этот список, а не ссылку.

Ответ 4

Для этого вам понадобится эталонная семантика вместо семантики значений. Самый простой способ реализовать ссылочную семантику - через указатели. Только некоторые небольшие изменения в вашем примере сделают это:

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;
}