本文为《C++ Primer》的读书笔记
设计思路
作为类需要拷贝控制的例子, 我们将概述两个类的设计, 这两个类可能用于邮件处理应用中
两个类命名为Message
和Folder
, 分别表示电子邮件消息和消息目录。每个Message
对象可以出现在多个Folder
中。但是,任意给定的Message
的内容只有一个副本
为了记录Message
位于哪些Folder
中, 每个Message
都会保存一个它所在Folder
的指针的set
, 同样的, 每个Folder
都保存一个它包含的Message
的指针的set
我们的Message
类会提供save
和remove
操作, 来向一个给定Folder
添加一条或是删除一条Message
。为了创建一个新的Message
, 我们会指明消息内容, 但不会指出Folder
。为了将一条Message
放到一个特定Folder
中, 我们必须调用save
-
当我们拷贝一个
Message
时, 副本和原对象将是不同的Message
对象, 但两个Message
都出现在相同的Folder
中。因此, 拷贝Message
的操作包括消息内容和Folder
指针set
的拷贝。而且,我们必须在每个包含此消息的Folder
中都添加一个指向新创建的Message
的指针 -
销毁一个
Message
时, 必须从包含此消息的所有Folder
中删除指向此Message
的指针 -
当我们将一个
Message
对象赋予另一个Message
对象时,左侧Message
的内容会被右侧Message
的内容所替代。我们还必须更新Folder
集合,从原来包含左侧Message
的Folder
中将它删除, 并将它添加到包含右侧Message
的Folder
中
我们可以看到, 析构函数和拷贝赋值运算符都必须从包含一条Message
的所有Folder
中删除它。类似的,拷贝构造函数和拷贝赋值运算符都要将一个Message
添加到给定的一组Folder
中。我们将定义两个private
的工具函数来完成这些工作
拷贝赋值运算符通常执行拷贝构造函数和析构函数中也要做的工作。这种情况下, 公共的工作应该放在
private
的工具函数中完成
Folder
类也需要类似的拷贝控制成员, 来添加或删除它保存的Message
。假定Folder
类包含名为addMsg
和remMsg
的成员
Message
类
class Message {
friend class Folder;
public:
// folders被隐式初始化为空集合
explicit Message(const std::string &str = ""):
contents(str) { }
// 拷贝控制成员, 用来管理指向本Message的指针
Message(const Message&);
Message& operator= (const Message&);
~Message();
// 从给定Folder集合中添加/删除本Message
void save(Folder&);
void remove(Folder&);
private:
std::string contents; // 实际消息文本
std::set<Folder*> folders; // 包含本Message的Folder
// 拷贝构造函数、拷贝赋值运算符和析构函数所使用的工具函数
// 将本Message添加到指向参数的Folder中
void add_to_Folders(const Message&);
// 从folders中的每个Folder中删除本Message
void remove_from_Folders();
};
void Message::save(Folder &f)
{
folders.insert(&f); //将给定Folder添加到我们的Folder 列表中
f.addMsg(this); //将本Message添加到f的Message 集合中
}
void Message::remove(Folder &f)
{
folders.erase(&f); //将给定Folder从我们的Folder 列表中删除
f.remMsg(this); //将本Message从f的Message 集合中删除
}
// 将本Message添加到指向m的Folder中
void Message::add_to_Folders(const Message &m)
{
for (auto f : m.folders) //对每个包含m的Folder
f->addMsg(this); //向该Folder添加一个指向本Message的指针
}
Message::Message(const Message &m):
contents(m.contents), folders(m.folders)
{
add_to_Folders(m); // 将本消息添加到指向m的Folder中
}
// 从对应的Folder 中删除本Message
void Message::remove_from_Folders()
{
for (auto f : folders) //对folders 中每个指针
f->remMsg(this); //从该Folder 中删除本Message
}
// 编译器自动调用string的析构函数来释放contents,
// 并自动调用set的析构函数来清理集合成员使用的内存
Message::~Message()
{
remove_from_Folders();
}
Message& Message::operator=(const Message &rhs)
{
// 通过先删除指针再插入它们来处理自赋值情况
remove_from_Folders(); //更新已有Folder
contents = rhs.contents; //从rhs拷贝消息内容
folders = rhs.folders; //从rhs拷贝Folder 指针
add_to_Folders(rhs); //将本Message添加到那些Folder中
return *this;
}
void swap(Message &lhs, Message &rhs)
{
using std::swap; // 在本例中严格来说并不需要, 但这是一个好习惯
// 将每个消息的指针从它(原来)所在Folder中删除
for (auto f : lhs.folders)
f->remMsg(&lhs);
for (auto f: rhs.folders)
f->remMsg(&rhs);
// 交换contents和Folder指针set
swap(lhs.folders, rhs.folders); // 使用swap(set&, set&)
swap(lhs.contents, rhs.contents); // swap(string&, string&)
// 将每个Message的指针添加到它的(新) Folder中
for (auto f: lhs.folders)
f->addMsg(&lhs);
for (auto f: rhs.folders)
f->addMsg(&rhs);
}
Message
类的移动操作
定义了自己的拷贝构造函数和拷贝赋值运算符的类通常也会从移动操作受益。通过定义移动操作,Message
类可以使用string
和set
的移动操作来避免拷贝contents
和folders
成员的额外开销。但是, 除了移动folders
成员, 我们还必须更新每个指向原Message
的Folder
。我们必须删除指向旧Message
的指针, 并添加一个指向新Message
的指针
移动构造函数和移动赋值运算符都需要更新Folder
指针,因此我们首先定义一个操作来完成这一共同的工作:
// 从本Message移动Folder指针
void Message::move_Folders(Message *m)
{
folders = std::move(m->folders); //使用set的移动赋值运算符
for (auto f : folders) { //对每个Folder
f->remMsg(m); //从Folder中删除旧Message
f->addMsg(this); //将本Message添加到Folder中
}
m->folders.clear(); // 确保销毁m是无害的
// 由于Message的析构函数遍历folders, 我们希望能确定set是空的
}
值得注意的是, 向set
插入一个元素可能会抛出一个异常(bad_alloc
)。因此,Message
的移动构造函数和移动赋值运算符可能会抛出异常。因此我们未将它们标记为noexcept
Message::Message(Message &&m): contents(std::move(m.contents))
{
move_Folders(&m); // 移动folders并更新Folder指针
}
Message& Message::operator=(Message &&rhs)
{
if (this ! = &rhs) { // 直接检查自赋值情况
remove_from_Folders(); // 销毁左侧运算对象的旧状态
contents = std::move(rhs.contents); // 移动赋值运算符
move_Folders(&rhs); // 重置Folders指向本Message
return *this;
}