【问题标题】:push_back/emplace_back a shallow copy of an object into another vectorpush_back/emplace_back 一个对象的浅拷贝到另一个向量中
【发布时间】:2017-01-06 19:25:07
【问题描述】:

假设我有以下代码

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 的任何元素。我认为最简单的方法是让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

【问题讨论】:

  • 使用std::vector&lt;std::shared_ptr&lt;Car&gt;&gt;,你就会得到你想要的。
  • 在 Python 中你一直在处理指针,在 Python 语言中它们只是不被称为指针。因此,为了模拟 Python,您只需要停止调用指针指针,并且足够用力地眯眼,以使 -&gt; 看起来像 .。 (Python 指针是“智能的”(垃圾收集),您也需要在 C++ 中使用智能指针,但这是一个小细节。
  • @n.m.所以你建议我仍然做“让collection1和collection2成为指针向量并指向Car的同一个向量”,然后将这个“Car向量”设为静态,以便我可以在返回上一个函数后访问它?
  • C++ 中确实存在“浅”和“深”副本。有副本,做一个类的复制构造函数所做的任何事情。看来您需要存储不同的类型(即指针或reference_wrapper。)
  • @Mine 这应该是一个答案

标签: python c++ vector


【解决方案1】:

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

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

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

或者,如果集合不是对称的——也就是说,如果一个集合实际上包含所有对象,而另一个只包含对前一个对象的引用——引用将是你的最佳选择打赌,第二个集合可能是std::vector&lt;std::reference_wrapper&lt;Car&gt;&gt;


添加每个 MvG 评论。

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

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

a 包含对 b 的引用,其中包含对 a 的引用...

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

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

但如果 a 和 b 都被删除(或超出范围),gc 将检测到没有更多的外部 ref 并将销毁所有内容。

不幸的是,如果你设法使用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!

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

【讨论】:

  • 提一下 shared_ptr 不检测循环可能是个好主意,这与 Python 引用相反。
  • @MvG 你能详细说明一下吗? (我对python不太熟悉)。
【解决方案2】:

既然你提到你不喜欢指针,你可以使用引用,但向量不能存储引用(因为它们不可复制和分配)。但是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()};

使用这种方式,collection2collection1 引用相同的对象。例如:

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

重要:

请注意,不鼓励使用这种方式,因为您必须有另一个实体来管理对collection1 的插入和删除,并对collection2 采取适当的操作。 @Serge Ballesta 的回答比我的好。使用std::shared_ptr。试着去爱和拥抱指针:)

【讨论】:

  • 这是糟糕的风格和糟糕的代码。一旦collection1 被更改,collection2 将包含悬空引用。这是一个严重的问题,应该从设计的根本上避免。
  • @Walter 你说得对,但我不是 OP 项目的 设计师。我只是假设collection1 的插入和删除是由另一个实体同步和管理的。
  • @Walter 另外,在这种情况下,我个人更喜欢std::shared_ptr,它非常安全和方便,但 OP 似乎没有。
  • 你推荐糟糕的代码。这是不可接受的。而NO,不是因为OP的项目的设计,而是因为你选择使用std::reference_wrapper&lt;&gt;
  • @Walter:共享指针会带来引用计数操作的时间和空间开销。如果一个集合的生命周期显然是另一个集合的超集,那么这种开销是可以避免的,并且在性能关键的情况下应该避免。这是典型的性能与安全权衡,类似于v[i]v.at(i)。就我个人而言,我会使用指针集合,但在我看来,指针和包装引用之间的区别可以忽略不计,并且在某些情况下,更好的表示法(更少的* 间接)可能会使使用引用包装器成为更好的解决方案。
【解决方案3】:

简短的回答:你不能在 C++ 中完全模拟 python

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

在 C++ 中,同样的设计也可以实现。因为只要对它的任何引用仍然存在,一个对象就必须保持活动状态,但是一旦最后一个引用超出范围就会被删除(并释放任何内存),因此这些对象是共享的。 C++ 处理共享对象的方式是通过std::shared_ptr&lt;T&gt;。为什么 this 必须是类指针对象而不是类引用对象(例如 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&lt;T&gt; 的行为类似于指针,但这与引用非常相似(在语法上与指针不同,但实现方式相同)。


请注意,不可能在 C++ 中设计相应的 shared_reference&lt;T&gt;,其功能与 python 变量相同,即类似于 std::shared_ptr&lt;T&gt;,但使用 . 而不是 -&gt; 并使用保证有效对象(无空/空引用/指针)。原因是. 运算符不能重载。例如

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++ 的本质。这意味着对于间接,您应该使用指针。

【讨论】:

  • 你可以选择这样的东西:shared_reference&lt;car&gt;.get().color = "blue"; 使用起来有点笨拙,但实现起来非常简单。
【解决方案4】:

与其他人所说的使用 std::reference_wrapper&lt;T&gt; 类似但不同,这可能有用,但有人在您的问题下面的 cmets 中也提到了这一点,那就是使用智能指针,这里唯一的区别是我碰巧采取了通过创建模板包装类更进一步。这是代码,它应该做你正在寻找的东西,除了这是在堆上工作而不是使用引用。

#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&lt;T&gt;每次都这样做;模板包装类会为您执行此操作,您不必担心清理内存,因为std::shared_ptr&lt;T&gt;'s destructor 应该为您执行此操作,但为了安全起见,我确实在Wrapper's destructor 中调用了shared_ptr&lt;T&gt;'s release method

为了使它更简洁或更具可读性,您可以这样做:

typedef Wrapper<Car> car;

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

// rest is same

它也会为你做同样的事情。

现在,如果您不想使用指针或堆,您可以自己创建另一个类似于std::refrence_wrapper&lt;T&gt; 的包装器,您可以为引用编写自己的模板包装器,使用起来非常简单。这是一个例子:

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 在 cmets 中问过我这个问题:

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     
}

但是,创建一个将 reference 存储到 obj Tclass template,您现在可以这样做:

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&lt;T&gt; 中的引用之前无法做到这一点,除非你使用std::reference_wrapper&lt;T&gt;,它的作用基本相同,但更复杂。所以对于简单的对象,拥有自己的包装器可以派上用场。

编辑 - 在我的 IDE 中工作时我忽略了并且没有注意到这一点,因为所有内容都已成功编译、构建和运行,但我注意到这个问题的 OP 应该完全无视我的第二个包装。这可能导致未定义的行为。因此,您仍然可以使用智能指针的第一个包装器,或者如果您需要其他人已经指出的可存储引用,请务必使用std::some_container&lt;std::reference_wrapper&lt;T&gt;&gt;。我将上面的现有代码留作历史参考,供其他人学习。我非常感谢那些指出未定义行为的人。对于那些不知道的人,请考虑到我没有接受过正规培训,而且我是 100% 自学的并且还在学习。您也可以在这里参考我就引用和未定义行为提出的这个问题:undefined behavior of references on stack

结论

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

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

【讨论】:

  • Wrapper 比这里的 shared_ptr 有什么好处? (和 Wrapper2 在 reference_wrapper 上)
  • @Caleth 没有太多,但shared_ptr 包装器的一个好处是它的构造函数会自动为您调用std::make_shared&lt;T&gt;。这样,如果您确实有一个 vector&lt;shared_ptr&lt;T&gt;&gt;,您就不必在每次要将某些内容推入该向量时调用 make_shared&lt;T&gt;,包装类会为您执行此操作。至于参考,它做了它应该做的事情。你不能有一个vector&lt;class &amp;T&gt;,但是使用这个包装器,类似于std::reference_wrapper&lt;T&gt;,当你有同一个对象的多个容器时,你可以看到,改变一个会改变另一个。
  • 在您的第二个示例中,您有悬空引用。您正在构建临时 blob
  • 你的答案太长而且很混乱(这是几个答案合二为一)。你的Wrapper2 设计很糟糕,容易出现悬空引用。 C++ 程序员甚至不应该考虑这样的设计。
  • @FrancisCugler 我也没有。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2012-07-06
  • 2018-02-03
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-10-04
相关资源
最近更新 更多