T230723 copy-and-swap Idiom and More Tricks
今天讲一个 Idiom 加一些 Tricks。
本次内容紧紧围绕着 The Rule of the Big Five,即
- destructor
- copy constructor
- copy assignment constructor
- move constructor
- move assignment constructor
要讲的 Idiom 主要是针对后三个,Tricks 则主要是针对后两个,属于是一些额外的优化技巧。
先从 The Big 3 说起,就是前三个,由此引出 Idiom 存在的必要性。一个小例子:
class my_string
{
public:
// default ctor
my_string(const char* str = "")
: size_ { std::strlen(str) }
, str_ { size_ ? new char[size_] : nullptr }
{
std::copy(str, str + size_, str_);
}
// copy ctor
my_string(const my_string& other)
: size_ { other.size_ }
, str_ { size_ ? new char[size_] : nullptr }
{
std::copy(other.str_, other.str_ + size_, str_);
}
// copy assignment
my_string& operator=(const my_string& other)
{
if (this != &other)
{
// delete the old data
delete[] str_;
str_ = nullptr;
// put the new data
size_ = other.size_;
str_ = size_ ? new char[size_] : nullptr;
std::copy(other.str_, other.str_ + size_, str_);
}
return *this;
}
~my_string()
{
delete[] str_;
str_ = nullptr;
}
private:
std::size_t size_;
char* str_;
};
很经典的一个简单 String 类,没啥难点,不多论述,直接看问题所在。default ctor 和 copy ctor 都比较简单,当前的问题主要在于 copy assignment。
第一,存在一个 self-assignment 检查,这意味着每次赋值时,都会进行一次检查,而遇到 self-assignment 的情况实属罕见,这就等同于为了那么一点醋,包了顿饺子,颇为浪费;第二,没有异常安全保证,如果 new char[size_]
失败,原 this
中的数据已经被改变了;第三,代码重复,相关代码在 default ctor 和 copy ctor 已经写过了,这里又写了一次,违背 DRY(Don’t Repeat Yourself) 原则。
对于第二点,还比较容易修正,只要在发生错误之前别修改 this
的数据就能够解决。
// copy assignment
my_string& operator=(const my_string& other)
{
if (this != &other)
{
// put the new data into a temporary allocation.
std::size_t new_size = other.size_;
char* new_str = new_size ? new char[new_size] : nullptr;
std::copy(other.str_, other.str_ + new_size, new_str);
// Anything is ok, now we delete the old data
delete[] str_;
size_ = new_size;
str_ = new_str;
}
return *this;
}
但是其他的问题依旧存在。
而 copy-and-swap Idiom 可以优雅地解决以上三个问题,它的实现为:
class my_string
{
public:
// ...
// copy assignment
// copy-and-swap
my_string& operator=(my_string other) noexcept
{
swap(*this, other);
return *this;
}
// hidden friend
friend void swap(my_string& lhs, my_string& rhs) noexcept
{
using std::swap;
swap(lhs.size_, rhs.size_);
swap(lhs.str_, rhs.str_);
}
// ...
};
它的原理是什么呢?
首先,需要编写一个额外的 Hidden friend 函数,也是老生常谈的陈年问题妥协技巧了。
然后,将参数由 const&
变为 by value,从而能够直接利用 copy ctor 来消除重复代码,这样一来,在进入 copy assignment 函数之前,数据已经拷贝完成了。原来是自己手动编写拷贝代码,通过 by value,就可以让编译器自动来做,这样异常安全也由编译器保证,效果更好。而且 std::swap
还是 noexcept
,所以现在的函数非常安全。
最后,完成实际的调用。通过前两步,已经解决了问题二和问题三,同时也顺便解决了问题一。通过 by value 拷贝了一份数据,此时 self-assignment 检查已经没有必要了。之前检查主要是为了防止释放 this
,如今在进入函数之前,数据已经拷贝好了,之后也只是交换一下,根本就不可能再释放 this
。此外,by value 还能够自动选择是使用 move ctor, 还是 copy ctor。
这就是 copy-and-swap Idiom。
接着来说 Big 5 的后两个。
class my_string
{
public:
// ...
// move ctor
my_string(my_string&& other)
: my_string() // ctor delegation
{
swap(*this, other);
}
// move assignment
my_string& operator=(my_string&& other)
: my_string() // aha?
{
swap(*this, other);
return *this;
}
// ...
};
同样是借助 copy-and-swap Idiom。move 的核心是先赋值再清空,先通过委托构造将当前 this
声明为一个空值,然后再交换就能达到这个目的,但是委托构造只能在构造函数上使用,move assignment 无法使用。
这就要引出我们的新 Tricks —— std::exchange
。
看下更优雅的代码:
// move ctor
my_string(my_string&& other) noexcept
: size_ { std::exchange(other.size_, 0) }
, str_ { std::exchange(other.str_, {}) }
{
}
// move assignment
my_string& operator=(my_string&& other) noexcept
{
// safe for this == &other
size_ = std::exchange(other.size_, 0);
str_ = std::exchange(other.str_, {});
return *this;
}
std::exchange
这个函数和 i++
的逻辑一样,设置新值,返回旧值。借助它,只需一步,就能够实现 copy-and-swap 的工作,避免编写额外的临时变量。
什么,你说还是有重复?请像前面定义 swap
一样,把它们抽象成一个单独的函数,但却无需将它们也定义为 Hidden friend。
到此,本次要分享的内容就结束了。不过这里只讲解了 std::exchange
的冰山一角,后面还有一篇单独介绍它的,敬请期待。