提问人:Vasilii Rogin 提问时间:4/21/2023 更新时间:4/21/2023 访问量:131
使用特殊的复制赋值运算符而不是简单的析构函数和就地构造函数的原因
Reasons to have special copy assignment operator instead of simple destructor and in-place constructor
问:
我有一个具有自己的资源管理的类:
class Lol
{
private:
// This is data which this class allocates
char *mName = nullptr;
public:
Lol(std::string str) // In constructor just copy data from string
{
auto cstr = str.c_str();
auto len = strlen(cstr);
mName = new char[len + 1];
strcpy(mName, cstr);
};
~Lol() // And of course clean up
{
delete[] mName;
}
}
我实现了复制构造函数,它只是复制托管数据:
Lol(const Lol &other)
{
auto cstr = other.mName;
auto len = strlen(cstr);
mName = new char[len + 1];
strcpy(mName, cstr);
};
我还需要实现复制分配运算符。我刚刚这样做了:
Lol &operator=(const Lol &other)
{
if (this == &other)
{
return *this;
}
// Clean up my resources
this->~Lol();
// And copy resources from "other" using already implemented copy constructor
new (this) Lol(other);
}
看起来这个复制分配运算符将适用于所有类。为什么我需要在复制赋值运算符中包含另一个代码?它的用例是什么?
答:
如果构造函数抛出,则必须捕获异常并以某种方式恢复(通过调用一些构造函数,可能是默认的构造函数),这使得它变得不那么优雅。未能调用构造函数将导致双重销毁和 UB。
此外,如果您从此类继承,或将其用作成员变量,则此方法会导致 UB:[[no_unique_address]]
如果满足以下条件,则对象 o1 可被对象 o2 透明地替换:...
— O1 和 O2 都不是潜在重叠的子对象......
可能重叠的子对象是:
— 基类子对象,或者
— 使用 no_unique_address 属性声明的非静态数据成员。
这本身不是 UB,但如果你的对象不是透明可替换的,那么重建的对象必须在使用前进行编辑,这是不切实际的(例如,如果它是一个自动变量,自动销毁将在没有 UB 的情况下发生→)。std::launder
std::launder
C++17 有更多的限制。如果您的类包含 const 或 reference 成员,您还需要 (C++17 [basic.life]/8.3
)。std::launder
如果您正在寻找通用赋值运算符,那么有一个。它被称为复制和交换习语。看:
MyClass &operator=(MyClass other) noexcept
{
std::swap(x, other.x); // Swap every member here.
return *this;
}
这既充当复制赋值,又充当移动赋值(如果您有相应的构造函数),并提供强异常保证(如果抛出复制,则目标对象保持不变)。
唯一一种情况(据我所知)它不能开箱即用是当类在某个地方(可能在自身内部)维护指向自己的指针时。
评论
Lol lol1{"lol1"}; Lol lol2{"lol2"}; lol1 = lol2;
lol2
lol1
lol1
使用 copy and swap 成语。
Lol &operator=(const Lol &other)
{
if (this == &other)
{
return *this;
}
// Clean up my resources
this->~Lol();
// Copy can throw.
// Then your object is in an undefined state.
new (this) Lol(other);
// You forgot the return:
return *this;
}
因此,这并不能提供强有力的(或任何)例外保证。
首选方式是:
Lol& operator=(Lol const& other)
{
Lol copy(other); // Here we use the copy constructor
// And the destructor at the end of
// function cleans up the scope
// Note this happens after the swap
// so you are cleaning up what was in
// this object.
swap(copy);
return *this;
}
void swap(Lol& other) noexcept
{
std::swap(mName, other.mName);
}
如今,我们已经改进了这个原始版本
Lol& operator=(Lol copy) // Notice we have moved the copy here.
{
swap(copy);
return *this;
}
令人兴奋的是,此版本的作业同样有效地适用于复制和移动作业。
评论
this->~Lol(); new (this) Lol(other);
--我不得不问--有这么简单的一堂课,突然想到这样做的想法是从哪里来的?使用 placement-new 的原因是什么?为什么不简单地:{ Lol temp(other); std::swap(mName, temp.mName); return *this; }
std::unique_ptr<char[]>
std::vector<char>
std::string