提问人:Joker_vD 提问时间:2/14/2014 最后编辑:Bernhard BarkerJoker_vD 更新时间:2/14/2014 访问量:138
这样的写作业有什么问题?
What are problems with writing assignment like this?
问:
前几天,我和我的一个朋友就对象分配和构造进行了一次对话,他指出,对象的分配(在语义上)等同于摧毁它,然后从(在同一个地方)重新构造它。a = b
a
b
但是,当然,没有人(我认为)像这样编写赋值运算符:
class A {
A& operator=(const A& rhs) {
this->~A();
this->A(rhs);
return *this;
}
A& operator=(A&& rhs) {
this->~A();
this->A(std::move(rhs));
return *this;
}
// etc.
};
[注意:我不知道如何在现有对象上手动调用构造函数/析构函数(我从来没有这样做过!),所以它们的调用可能没有形式意义,但我想你可以看到这个想法。
这种方法有什么问题?我想必须有一个主要的表演者,但名单越大越好。
答:
首先,只有当对象是使用 overload 构造的时,才需要手动调用析构函数,并具有一些期望,例如使用重载。operator new()
std::nothrow
你要理解的是复制构造和赋值运算符之间的区别:当从现有对象创建新对象时,将调用复制构造函数,作为现有对象的副本。当从另一个现有对象为已初始化的对象分配新值时,将调用赋值运算符。
总而言之,您提供的赋值运算符示例没有意义 - 它必须具有不同的语义。
如果您还有其他问题,请发表评论。
首先,直接调用复制构造函数是不合法的(至少在兼容 C++ 的编译器中是这样。VS2012 允许这样做),因此不允许以下情况:
// assignment operator
A& operator=(const A& rhs) {
this->~A();
this->A::A(rhs); <--- Invalid use
此时,您可以依赖编译器优化(请参阅复制省略和 RVO)或将其分配到堆上。
如果您尝试执行上述操作,可能会出现许多问题:
1) 您可能会在复制构造函数的表达式中抛出异常
在这种情况下,您将拥有
// assignment operator
A& operator=(const A& rhs) {
cout << "copy assignment called" << endl;
this->~A();
A newObj(rhs); // Can throw and A is in invalid state!
return newObj;
}
为了安全起见,您应该使用复制和交换的习惯用语:
set& set::operator=(set const& source)
{
/* You actually don't need this. But if creating a copy is expensive then feel free */
if (&source == this)
return;
/*
* This line is invoking the copy constructor.
* You are copying 'source' into a temporary object not the current one.
* But the use of the swap() immediately after the copy makes it logically
* equivalent.
*/
set tmp(source);
this->swap(tmp);
return *this;
}
void swap(set& dst) throw ()
{
// swap member of this with members of dst
}
2) 动态分配的内存可能有问题
如果 A 的两个实例共享一个指针,则在能够释放它之前,您可能有一个悬空的指针
a = a; // easiest case
...
// assignment operator
A& operator=(const A& rhs) {
this->~A(); <-- Freeing dynamically allocated memory
this->A::A(rhs); <--- Getting a pointer to nowhere
3)正如Emilio所指出的,如果类是多态的,你将无法重新实例化该子类(除非你用类似CRTP的技术以某种方式欺骗它)
4)最后,赋值和复制构造是两种不同的操作。如果 A 包含重新获取成本高昂的资源,您可能会发现自己陷入了很多麻烦。
评论
tmp
b
tmp
a
tmp
a
这里有一个误用的结构:
class A {
A& operator=(const A& rhs) {
if(&a==this) return *this;
this->~A();
new(this) A(rhs);
return *this;
}
A& operator=(A&& rhs) {
if(&a==this) return *this;
this->~A();
new(this) A(std::move(rhs));
return *this;
}
// etc.
};
这是对就地 ctor/dtor 语义的正确尊重,这就是在缓冲区中破坏和构造元素的作用,所以这必须是正确的,对吧?std::allocator
井。。。不正确:这一切都是关于 A 实际上包含的内容以及 A ctor 实际做什么。
如果 A 只包含基本类型并且不拥有资源,那很好,它可以工作。这不是惯用的,但是正确的。
如果 A 包含一些其他资源,需要好好获取、管理和释放......你可能会遇到麻烦。如果 A 是多态的,你也是(如果 ~A 是虚拟的,你破坏整个对象,但随后你只重建 A 子对象)。
问题在于,获取资源的构造函数可能会失败,而在构造中失败并抛出的对象不能被销毁,因为它从未被“构造”过。
但是,如果你在“分配”,你就不是在“创建”,如果就地 ctor 失败,你的对象将存在(因为它预先存在于它自己的范围内),但处于无法通过进一步销毁来管理的状态:想想看
{
A a,b;
a = b;
}
在 b 和 a 将被销毁,但如果 A(const A&) 在 a=b 中失败,并且 a 在 中不存在,但会在 被不正确地销毁,这将立即跳转到。}
throw
A::A
a
}
throw
一种更惯用的方法是拥有
class A
{
void swap(A& s) noexcept
{ /* exchanging resources between existing objects should never fail: you just swap pointers */ }
public:
A() noexcept { /* creates an object in a "null" recognizable state */ }
A(const A& s) { /* creates a copy: may fail! */ }
A(A&& s) noexcept { /*make it as null and... */ swap(s); } // if `s` is temporary will caryy old resource deletionon, and we keep it's own resource going
A& operator=(A s) noexcept { swap(s); return *this; }
~A() { /* handle resource deletion, if any */ }
};
现在
a=b
将创建一个副本作为 中的参数(通过 )。
如果这失败了,将不存在并且仍然有效(具有自己的旧值),因此在范围退出时将像往常一样被销毁。
如果复制成功,则复制的资源和实际的资源将被交换,当死亡时,旧的资源将被释放。b
s
operator=
A::A(const A&)
s
a
b
a
s
}
作者:converse
a = std::move(b)
将作为临时的,通过 A(A&&) 构造的参数,因此 b 将与 s 交换(并变为 null),而不是 s 将与 a 交换。最后,将销毁旧资源,接收旧资源,b 将处于空状态(因此当其范围结束时,它可以平静地死亡)b
s
s
a
a
b
“使 A 为 null”的问题必须在 和 中实现。
这可以通过帮助程序成员(一个 ,就像 一样)或通过指定成员初始值设定项,或者通过定义成员的默认初始化值(一次)A()
A(A&&)
init
swap
评论
swap
A::A(A&& rhs)
rhs
rhs
b
A
null
default-constructed
评论
if (reinterpret_cast<void*>(this) == reinterpret_cast<void*>(&rhs)) { return (*this); }