我正在学习c ++并且我最近学习了(这里是堆栈溢出)关于复制和交换的习惯用法,我有一些关于它的问题。所以,假设我使用了一个复制和交换习惯用语,例如:
class Foo {
private:
int * foo;
int size;
public:
Foo(size_t size) : size(size) { foo = new int[size](); }
~Foo(){delete foo;}
Foo(Foo const& other){
size = other.size;
foo = new int[size];
copy(other.foo, other.foo + size, foo);
}
void swap(Foo& other) {
std::swap(foo, other.foo);
std::swap(size, other.size);
}
Foo& operator=(Foo g) {
g.swap(*this);
return *this;
}
int& operator[] (const int idx) {return foo[idx];}
};
我的问题是,假设我有另一个类将Foo对象作为数据,但没有可能需要自定义复制或赋值的指针或其他资源:
class Bar {
private:
Foo bar;
public:
Bar(Foo foo) : bar(foo) {};
~Bar(){};
Bar(Bar const& other) : bar(other.bar) {};
Bar& operator=(Bar other) {bar = other.bar;}
};
现在我有一系列问题:
上面为Bar
类实现的方法和构造函数是否安全?使用Foo
的复制和交换后,我确保在分配或复制Bar
时不会造成任何伤害?
在复制构造函数和swap中通过引用传递参数是必需的吗?
是否正确地说当operator=
的参数按值传递时,将为此参数调用复制构造函数以生成对象的临时副本,并且该副本是然后与*this
交换?如果我在operator=
中通过引用,我会遇到一个大问题,对吗?
是否存在此习语无法在复制和分配Foo
时提供完全安全的情况?
答案 0 :(得分:8)
您应该尽可能在初始化列表中初始化类的成员。这也将照顾我在评论中告诉你的错误。考虑到这一点,您的代码变为:
class Foo {
private:
int size;
int * foo;
public:
Foo(size_t size) : size(size), foo(new int[size]) {}
~Foo(){delete[] foo;} // note operator delete[], not delete
Foo(Foo const& other) : size(other.size), foo(new int[other.size]) {
copy(other.foo, other.foo + size, foo);
}
Foo& swap(Foo& other) {
std::swap(foo, other.foo);
std::swap(size, other.size);
return *this;
}
Foo& operator=(Foo g) {
return swap(g);
}
int& operator[] (const int idx) {return foo[idx];}
};
和
class Bar {
private:
Foo bar;
public:
Bar(Foo foo) : bar(foo) {};
~Bar(){};
Bar(Bar const& other) : bar(other.bar) { }
Bar& swap(Bar &other) { bar.swap(other.bar); return *this; }
Bar& operator=(Bar other) { return swap(other); }
}
在整个
中使用相同的习语如问题评论中所述,Bar
的自定义副本构造函数等是不必要的,但我们假设Bar
还有其他内容: - )
需要通过引用传递swap
,因为两个实例都已更改。
需要通过引用传递复制构造函数,因为如果按值传递,则需要调用复制构造函数
是
不,但并不总是最有效的做事方式
答案 1 :(得分:5)
1 - 上面为Bar类实现的方法和构造函数是否安全?使用Foo的复制和交换后,我确保在分配或复制Bar时不会造成任何伤害?
关于copy-ctor:这总是安全的(全有或全无)。它要么完成(全部),要么抛出异常(没有)。
如果您的类只由一个成员组成(即没有基类),则赋值运算符将与成员类的安全性一样安全。如果您有多个成员,则赋值运算符将不再是“全有或全无”。第二个成员的赋值运算符可以抛出异常,在这种情况下,对象将被分配为“中途”。这意味着您需要在新类中再次实现复制和交换以获得“全部或全部”分配。
然而,在您不会泄漏任何资源的意义上,它仍然是“安全的”。当然,每个成员的状态将是一致的 - 只是新类的状态不一致,因为一个成员被分配而另一个成员没有被分配。
2 - 在复制构造函数和swap中通过引用传递参数是必需的吗?
是的,通过引用传递是强制性的。复制构造函数是复制对象的复制构造函数,因此不能按值接受它的参数,因为这意味着必须复制参数。这将导致无限递归。 (拷贝监视器将被调用参数,这意味着要为参数调用copy-ctor,这意味着......)。对于swap,原因是另一个:如果你要按值传递参数,你永远不能使用swap来真正交换两个对象的内容 - 交换的“目标”将是最初传入的对象的副本,这将被立即销毁。
3 - 是否正确地说当operator =的参数通过值传递时,为此参数调用复制构造函数以生成对象的临时副本,并且该副本随后与之交换*这个?如果我在operator =中通过引用传递我会有一个大问题,对吗?
是的,没错。然而,通过引用-const获取参数,构造本地副本然后与本地副本交换也是很常见的。 by reference-to-const 方式有一些缺点(它禁用了一些优化)。如果你不实现copy-and-swap,你应该通过reference-to-const传递。
4 - 在复制和分配Foo时,是否存在这种成语无法提供完全安全的情况?
我不知道。当然,总是可以通过多线程(如果没有正确同步)使事情失败,但这应该是显而易见的。
答案 2 :(得分:2)
这是一个典型的例子,说明跟随成语导致不必要的性能惩罚(过早的悲观化)。这不是你的错。复制和交换习语过于夸张。 是一个很好的习语。但是盲目地应该不。
注意:您可以在计算机上执行的最昂贵的事情之一是分配和释放内存。在实践中避免这样做是值得的。
注意:在您的示例中,复制和交换习惯用法总是执行一次释放,并且通常(当赋值的rhs是左值时)也会执行一次分配。
观察:当size() == rhs.size()
时,不需要重新分配或分配。您所要做的就是copy
。这很多,速度更快。
Foo& operator=(const Foo& g) {
if (size != g.size)
Foo(g).swap(*this);
else
copy(other.foo, other.foo + size, foo);
return *this;
}
即。检查您可以先回收资源的情况。如果无法回收资源,请复制并交换(或其他)。
注意:我的评论并不与其他人给出的好答案相矛盾,也没有任何正确性问题。我的评论只是一个重大的性能损失。