push_back / emplace_back将对象的浅表副本复制到另一个向量中

时间:2017-01-05 06:14:59

标签: python c++ vector

说我有以下代码

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]的数据字段将消失。我只想要这个&#34;黑色福特&#34;存在于两个向量中,并且通过任一向量对该特定对象所做的更改将反映在两个向量上。

我猜测对于真实对象的向量,此向量的元素占用实际内存。因此collection1的元素必须独立于collection2的任何元素。我认为最简单的方法是让collection1collection2成为指针的向量,并指向Car的相同向量。但是有没有可能的方法使上面的代码工作,而不使用指针向量。我最终希望将这两个集合都返回到上一个函数,因此制作指针向量是没有意义的。

简而言之,我想在python中模拟List.append()方法。

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

4 个答案:

答案 0 :(得分:13)

在C ++语言中,标准集合实际上包含对象,而在其他语言(如Python或Java)中,它们实际上包含引用(或指针)到存储在别处的对象。但由于C ++不包含垃圾收集,因此必须明确管理对象的生命周期其他

该设计的结果是,为了允许在两个不同的集合中使用相同的对象,必须使用指针或引用的集合(注意,C ++不直接允许引用集合;但是,为此创建了std::ref

根据您的使用情况,您可以使用原始指针(如果已经管理了实际对象的生命周期),也可以使用内部管理引用计数的智能指针(此处为std::shared_ptr)来确保当最后一个shared_ptr被销毁时,该对象将被自动销毁。这与Python对对象的引用相差不远,前提是您知道最后shared_ptr的破坏实际上会破坏对象(*)。换句话说,如果你不想让它变成悬空,不要保留任何其他指针或引用。

或者,如果集合不是对称的 - 也就是说,如果一个实际上包含所有对象,而另一个只包含对前一个对象的引用 - 引用将是你最好的下注,第二个集合可以是std::vector<std::reference_wrapper<Car>>

根据MvG评论添加。

Python对象和C ++ shared_ptr之间可能存在令人讨厌的差异。 Python有一个完整的垃圾收集器,它足够聪明,可以在没有外部引用时立即检测循环引用并破坏循环。例如:

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

a包含一个ref to b,其中包含一个ref ... ...

如果a被删除(或超出范围),b仍将包含完整的链

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

但如果a和b都被删除(或超出范围),gc将检测到没有更多的外部参考并且会破坏所有内容。

不幸的是,如果你设法使用std::shared_ptr构建一个C ++对象循环,因为它只使用本地引用计数,每个对象都会有一个引用另一个对象,即使它们出去也永远不会被删除范围将导致内存泄漏。一个例子:

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!

Hell来到这里:即使a和b都超出范围,ref计数仍然是1,共享指针永远不会删除它们的对象,没有循环引用通常会发生什么。程序员必须明确地处理并打破循环(并禁止它发生)。例如,在b超出范围之前b.next = make_shared<Node>();就足够了。

答案 1 :(得分:6)

由于你提到你不喜欢指针,你可以使用引用,但是向量不能存储引用(因为它们不可复制和可分配)。但是std::reference_wrapper在可复制和可分配对象中包装引用。

  

std::reference_wrapper是一个类模板,它在可复制的可分配对象中包装引用。它经常被用作在标准容器(如std::vector)中存储引用的机制,它通常不能保存引用。

     

来源: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采取适当的操作。 @ Serge Ballesta的回答比我好。使用std::shared_ptr。尝试爱和拥抱指针:)

答案 2 :(得分:3)

简短回答:你无法在C ++中完全模拟python

与python变量不同,C ++变量是真正的对象,而不仅仅是对象的引用,其复制对底层对象没有任何作用,因此总是浅的(此外,python使用类型擦除来允许其变量引用任何变量)可能的对象)。

在C ++中,也可以实现这种相同的设计。由于对象必须保持活动状态,只要对它的任何引用仍然存在,但是一旦最后一个引用超出范围就被删除(并释放任何内存),这些对象 shared 。处理共享对象的C ++方法是通过 std::shared_ptr<T> 。请参阅下文,了解为什么它必须是类似指针的对象而不是类似引用的对象(例如python变量)。

因此,以C ++方式使用C ++,您的代码将是

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>的行为与指针类似,但这与引用非常相似(它与语法中的指针不同,但实现方式相同)。

请注意,无法在C ++中设计具有与python变量相同功能的相应shared_reference<T> ,即类似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";

的工作。这就是C ++的简单方法。这意味着对于间接,你应该使用指针。

答案 3 :(得分:-2)

类似但不同于其他人对使用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>'s destructor应该为您执行此操作,但为了安全起见,我确实调用了shared_ptr<T>'s release method Wrapper's 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的第一个索引对象的字段也在被更改。

修改

@Caleth在评论中问我:

  

包装器在shared_ptr上有什么好处? (和reference_wrapper上的Wrapper2)

没有这个包装器,请看这里的代码:

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 templatereference的{​​{1}},您现在可以执行此操作:

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>基本上做了同样的事情,但更复杂。因此对于拥有自己的包装器的简单对象可以派上用场。

编辑 - 在我的IDE中工作时,我忽略了一些并没有抓住这个,因为所有编译,构建和运行成功但是我已经注意到了这个OP问题应该完全忽略我的第二个包装器。这可能导致未定义的行为。因此,您仍然可以使用智能指针的第一个包装器,或者如果您需要可存储的引用,因为其他人已经指出肯定使用std::reference_wrapper<T>。我将把上面的现有代码留给其他人学习的历史参考。我感谢那些参与指出未定义行为的人。对于那些不了解的人,请考虑到我没有接受过正式培训,而且我100%自学并且还在学习。您还可以参考我在此处提出的有关引用和未定义行为的问题:undefined behavior of references on stack

<强>结论

尝试在多个容器中使用相同对象的引用可能是个坏主意,因为当从任何容器中添加或删除某些内容时,它会导致未定义行为,从而留下悬空引用。因此,正确或更安全的选择是使用std::some_container<std::reference_wrapper<T>>来实现您想要的功能。

使用引用没有任何问题,但需要特别注意和设计,特别是有关被引用对象的生命周期。如果移动对象然后访问引用,这将导致问题,但如果您知道对象的生命周期并且不会移动或销毁它,那么访问引用不是问题。我仍建议您使用std::shared_ptr<T>std::shared_ptr