Push_back/emplace_back - мелкая копия объекта в другой вектор

Скажем, у меня есть следующий код

class Car {
    public:
        string color;
        string name;
        Car(string c, string n): color(c), name(n){}            
}

int main() {
    vector<Car> collection1;
    vector<Car> collection2;
    collection1.emplace_back("black", "Ford");
    collection1.emplace_back("white", "BMW");
    collection1.emplace_back("yellow", "Audi");

    //Question comes here
    collection2.push_back(collection1[0]);

}

Теперь я считаю, что это делает глубокую копию collection1[0]. Я попытался использовать collection2.emplace_back(move(collection1[0])), но тогда поля данных collection1[0] исчезнут. Я просто хочу, чтобы этот "черный Форд" существовал в обоих векторах, и изменения, внесенные в этот конкретный объект через любой вектор, отражались бы на обоих векторах.

Я предполагаю, что для вектора реальных объектов элементы этого вектора принимают фактическую память. Поэтому элемент collection1 должен быть независимым от любого элемента collection2. Я думаю, что самый простой способ - позволить collection1 и collection2 быть векторами указателей и указывать на один и тот же вектор Car. Но есть ли какие-либо возможные способы сделать работу над этим кодом без использования вектора указателей. В конечном итоге я хочу вернуть обе эти коллекции обратно к предыдущей функции, поэтому создание векторов указателей бессмысленно.

Короче говоря, я хочу имитировать метод List.append() в python.

collection1 = [Car("black", "Ford"),Car("white", "BMW"),Car("yellow", "Audi")]
collection2 = []
collection2.append(collection1[0])
collection2[0].color = "blue" // This affects collection1 as well

Ответ 1

В языках С++ стандартные коллекции фактически содержат объект, а на других языках, таких как Python или Java, они фактически содержат ссылки (или указатели) на объекты, которые хранятся в другом месте. Но поскольку С++ не включает сбор мусора, время жизни объекта должно быть явно разрешено в другом месте.

Следствием этого дизайна является то, что для того, чтобы один и тот же объект использовался в двух разных коллекциях, вы должны использовать коллекции указателей или ссылок (будьте осторожны, С++ не позволяет напрямую сбор ссылок, однако для этого было создано std::ref).

В зависимости от вашего варианта использования вы можете использовать необработанные указатели (если время жизни фактических объектов уже управляется) или использовать интеллектуальные указатели (здесь std::shared_ptr), которые внутренне управляют счетчиком ссылок, чтобы гарантировать, что объект автоматически уничтожается при уничтожении последнего shared_ptr. Это недалеко от ссылок на объекты Python, если вы знаете, что уничтожение последнего shared_ptr фактически уничтожит объект (*). Говоря иначе, не держите никаких других указателей или ссылок на него, если вы не хотите, чтобы он стал болтаться.

В качестве альтернативы, если коллекции не являются симметричными, то есть, если на самом деле они будут содержать все объекты, тогда как другие будут содержать только ссылки на объекты из прежних одностраничных ссылок, это будет ваш лучший выбор, а вторая коллекция может a std::vector<std::reference_wrapper<Car>>.


Добавление в комментарий MvG.

Существует некоторая разница между объектами Python и С++ shared_ptr. Python имеет полный сборщик мусора, который достаточно умен, чтобы обнаруживать циклические ссылки и уничтожает цикл, как только нет внешних ссылок. Пример:

>>> b = ['x']
>>> a = ['y']
>>> b.append(a)
>>> a.append(b)
>>> a
['y', ['x', [...]]]
>>> b
['x', ['y', [...]]]

a содержит ссылку на b, которая содержит ref для a...

Если a удаляется (или выходит за рамки), b все равно будет содержать целую цепочку

>>> del a
>>> b
['x', ['y', [...]]]

но если оба a и b удаляются (или выходят за рамки), gc обнаружит, что больше нет внешнего ref и уничтожит все.

К сожалению, если вам удастся построить цикл объектов С++ с помощью std::shared_ptr, поскольку он использует только локальный подсчет ref, каждый объект будет иметь ссылку ref на другой, и они никогда не будут удалены, даже когда они выйдут из области видимости что приведет к утечке памяти. Пример этого:

struct Node {
    int val;
    std::shared_ptr<Node> next;
};

a = make_shared<Node>();  // ref count 1
b = make_shared<Node>();
a.next = std::shared_ptr<Node>(b);
b.next = std::shared_ptr<Node>(a); // ref count 2!

Ад пришел сюда: даже когда a и b оба выйдут из области видимости, счетчик ссылок будет по-прежнему единым, и общие указатели никогда не удалят свои объекты, что должно было нормально происходить без круговой ссылки. Программист должен прямо обращаться с этим и нарушать цикл (и запрещать его выполнение). Например, b.next = make_shared<Node>(); до того, как b выйдет из области видимости, будет достаточно.

Ответ 2

Поскольку вы упомянули, что вам не нравятся указатели, вы можете использовать ссылки, но векторы не могут хранить ссылки (потому что они не могут копироваться и присваиваться). Однако std::reference_wrapper обертывает ссылку в копируемый и назначаемый объект.

std::reference_wrapper - это шаблон класса, который обертывает ссылку в подлежащем копированию присваиваемом объекте. Он часто используется как механизм для хранения ссылок внутри стандартных контейнеров (например, std::vector), которые обычно не могут содержать ссылки.

source: http://en.cppreference.com/w/cpp/utility/functional/reference_wrapper

vector<Car> collection1;
collection1.emplace_back("black", "Ford");
collection1.emplace_back("white", "BMW");
collection1.emplace_back("yellow", "Audi");

vector<std::reference_wrapper<Car>> collection2{collection1.begin(),
                                                collection1.end()};

Используя этот способ, collection2 ссылается на те же объекты, что и collection1. Например:

collection1[0].name = "frogatto!";
std::cout << collection2[0].get().name;
// prints 'frogatto!'

Важно:

Обратите внимание, что использование этого способа не рекомендуется, так как у вас должен быть другой объект, который управляет вставкой и удалением в/из collection1 и принимает соответствующие действия на collection2. @Серж Баллеста лучше, чем моя. Используйте std::shared_ptr. Попытайтесь полюбить и обнять указатели:)

Ответ 3

Короткий ответ: вы не можете полностью эмулировать питон в С++.

В отличие от переменных python переменные С++ являются реальными объектами, а не просто ссылками (на объекты), копирование которых ничего не делает для базового объекта и, следовательно, всегда является мелким (более того, python использует стирание типа, чтобы позволить его переменным ссылаться на любые возможный объект).

В С++ эта же конструкция также может быть достигнута. Поскольку объект должен оставаться в живых, если какая-либо ссылка на него по-прежнему существует, но удаляется (и освобождается любая память), как только последняя ссылка выходит за пределы области видимости, такие объекты разделяются. Способ С++ для общих объектов - std::shared_ptr<T>. Ниже приведена причина, по которой это должен быть объект, похожий на указатель, а не ссылочный объект (например, переменные python).

Таким образом, используя С++ в С++, ваш код будет

std::vector<std::shared_ptr<Car>> collection1, collection2;

collection1.push_back(std::make_shared<Car>("black", "Ford"));
collection1.push_back(std::make_shared<Car>("white", "BMW"));
collection1.push_back(std::make_shared<Car>("yellow", "Audi"));

collection2.push_back(collection1[0]);
collection2[0]->color = "blue";
std::cout<<collection1[0]->color;   // "blue"

std::shared_ptr<T> ведут себя как указатели, но это очень похоже на ссылку (которая отличается от указателя в его синтаксисе, но реализована одинаково).


Обратите внимание, что невозможно создать в С++ соответствующий shared_reference<T> с той же функциональностью, что и переменные python, например, std::shared_ptr<T>, но используя . вместо -> и с помощью гарантия действительного объекта (без нулевой/пустой ссылки/указателя). Причина в том, что оператор . не может быть перегружен. Например

template<typename T>
struct shared_reference
{
  template<typename...Args>
  shared_reference(Args&&...args)
  : ptr(std::make_shared<T>(std::forward<Args>(args)...)) {}
private:
  std::shared_ptr<T> ptr;
};

тогда есть способ сделать код вроде

shared_reference<car> Car;
Car.color = "blue";

работа. Это просто, как С++. Это означает, что для косвенности вы должны использовать указатели.

Ответ 4

Подобно, но отличается от того, что другие говорили об использовании std::reference_wrapper<T> Это может быть полезно, но кто-то также упомянул об этом в комментариях ниже вашего вопроса и использует интеллектуальные указатели, единственное отличие здесь в том, что я случайно это шаг дальше, создав класс обертки шаблона. Вот код, и он должен делать то, что вы ищете, за исключением того, что это работает над кучей, а не с помощью ссылок.

#include <iostream>
#include <memory>
#include <string>
#include <vector>

class Car {
public:
    std::string color;
    std::string name;
    Car(){}  // Added Default Constructor to be safe.
    Car( std::string colorIn, std::string nameIn ) : color( colorIn ), name( nameIn ){}
};

template<class T>
class Wrapper {
public:
    std::shared_ptr<T> ptr;

    explicit Wrapper( T obj ) {
        ptr = std::make_shared<T>( T( obj ) );
    }

    ~Wrapper() {
        ptr.reset();
    }
};

int main () {

    std::vector<Wrapper<Car>> collection1;
    std::vector<Wrapper<Car>> collection2;

    collection1.emplace_back( Car("black", "Ford") );
    collection1.emplace_back( Car("white", "BMW") );
    collection1.emplace_back( Car("yellow", "Audi") );

    collection2.push_back( collection1[0] );

    std::cout << collection2[0].ptr->color << " " << collection2[0].ptr->name << std::endl;

    collection2[0].ptr->color = std::string( "green" );
    collection2[0].ptr->name  = std::string( "Gremlin" );

    std::cout << collection1[0].ptr->color << " " << collection1[0].ptr->name << std::endl;

    return 0;
}

Если вы заметили в коде, я сменил коллекцию 2 первых полей индексного объекта, а затем я распечатал первые поля объекта индекса 1, и они были изменены. Итак, что происходит в одной коллекции, произойдет в другой, поскольку они shared memory, используя std::shared_ptr<T>, единственная причина, по которой я помещаю ее в оболочку, заключается в том, что ее конструктор создаст для вас новую память при построении, чтобы у вас не было делать это каждый раз; класс обертки шаблона делает это для вас, и вам не нужно беспокоиться об очистке памяти, потому что std::shared_ptr<T> destructor должен сделать это для вас, но чтобы быть в безопасности, я действительно вызывал shared_ptr<T> release method в Wrapper destructor.

Чтобы сделать это немного более чистым или более читаемым, вы можете сделать это вместо:

typedef Wrapper<Car> car;

std::vector<car> collection1;
std::vector<car> collection2;

// rest is same

И он сделает то же самое для вас.

Теперь, если вы не хотите использовать указатели или кучу, вы можете создать другую оболочку самостоятельно, которая будет похожа на std::refrence_wrapper<T>, вы можете написать свою собственную оболочку шаблона для ссылок, которые очень просты в использовании. Вот пример:

template<class T>
class Wrapper2 {
public:
    T& t;
    explicit Wrapper2( T& obj ) : t(obj) {} 
};

Затем в вашем источнике вы будете делать то же, что и выше, и он все еще работает

typedef Wrapper2<Car> car2;
std::vector<car2> coll1;
std::vector<car2> coll2;

coll1.emplace_back( Car( "black", "Ford" ) );
coll1.emplace_back( Car( "white", "BMW" ) );
coll1.emplace_back( Car( "yellow", "Audi" ) );

coll2.push_back( coll1[0] );

std::cout << coll2[0].t.color << " " << coll2[0].t.name << std::endl;

coll2[0].t.color = std::string( "brown" );
coll2[0].t.name  = std::string( "Nova" );

std::cout << coll1[0].t.color << " " << coll1[0].t.name << std::endl;

И, изменяя сначала поля с индексированными объектами coll2, также меняются поля с первым индексированным полем coll1.

ИЗМЕНИТЬ

@Калет спросил меня это в комментариях:

Какая польза от Wrapper только через shared_ptr? (и Wrapper2 над reference_wrapper)

Без этой обертки посмотрите здесь этот код:

class Blob {
public:
    int blah;
    Blob() : blah(0) {}
    explicit Blob( int blahIn ) : blah( blahIn ) {}
};


void someFunc( ... ) {
    std::vector<std::shared_ptr<Blob>> blobs;        
    blobs.push_back( std::make_shared<Blob>( Blob( 1 ) ) );
    blobs.push_back( std::make_shared<Blob>( Blob( 2 ) ) );
    blobs.push_back( std::make_shared<Blob>( Blob( 3 ) ) );
}

Да, это читаемо, но много повторяющегося ввода, теперь с оберткой

void someFunc( ... ) {   
    typedef Wrapper<Blob> blob;        
    std::vector<blob> blobs;
    blobs.push_back( Blob( 1 ) );
    blobs.push_back( Blob( 2 ) );
    blobs.push_back( Blob( 3 ) );
}

Теперь что касается Wrapper только для ссылки; попробуйте сделать это:

void someFunc( ... ) {
    std::vector<int&> ints; // Won't Work     
}

Однако при создании class template, который хранит reference до obj T, вы можете это сделать:

void someFunc( ... ) {
    typedef Wrapper2<Blob> blob;
    std::vector<blob> blobs;

    blobs.push_back( Blob( 1 ) );
    blobs.push_back( Blob( 2 ) );

    // then lets create a second container
    std::vector<blob> blobs2;
    // Push one of the reference objects in container 1 into container two
    blobs2.push_back( blobs[0] );
    // Now blobs2[0] contains the same referenced object as blobs[0]
    // blobs[0].t.blah = 1, blobs[1].t.blah = 2 and blobs2[0].t.blah = 1
    // lets change blobs2[0].t.blah value
    blobs2[0].t.blah = 4;

    // Now blobs1[0].t.blah also = 4.
}

Вы не могли бы сделать это раньше со ссылками в std::vector<T>, если только вы не использовали std::reference_wrapper<T>, который делает в основном то же самое, но гораздо более запутанным. Таким образом, для простых объектов, имеющих свои собственные оболочки, могут пригодиться.

EDIT. Что-то, что я упустил и не понял этого, работая в моей среде IDE, потому что все скомпилировано, построено и успешно запущено, но мне пришло в голову, что ОП этого вопроса должен полностью игнорировать мою вторую обертку. Это может привести к Undefined Поведение. Таким образом, вы все еще можете использовать 1-ю оболочку интеллектуального указателя или если вам нужны хранимые ссылки, как уже указывали другие, определенно используйте std::some_container<std::reference_wrapper<T>>. Я оставлю существующий код выше для исторической справки для других, чтобы учиться. Я благодарю тех, кто участвовал, указывая на поведение Undefined. И для тех, кто не знает, учтите, что у меня нет формального обучения и что я на 100% самоучитель и все еще участвую. Вы также можете обратиться к этому вопросу, который я задал в отношении ссылок и поведения Undefined здесь: undefined поведение ссылок в стеке

Заключение

Попытка использовать ссылки на одни и те же объекты в нескольких контейнерах может быть плохой идеей, поскольку она может привести к undefined Поведение, когда что-то добавляется или удаляется из любого контейнера, оставляя оборванные ссылки. Поэтому правильным или безопасным выбором было бы использовать std::shared_ptr<T> для достижения требуемой функциональности.

Нет ничего плохого в использовании ссылок, но нужно особо учитывать особую осторожность и дизайн, особенно в отношении времени жизни объектов, на которые ссылаются. Если объекты перемещаются, а затем обращается к ссылке, это приводит к проблемам, но если вы знаете время жизни объекта и что оно не будет перемещено или уничтожено, доступ к ссылкам не будет проблемой. Я бы предложил использовать std::shared_ptr или std::reference_wrapper