今天讲一个 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 的冰山一角,后面还有一篇单独介绍它的,敬请期待。

Leave a Reply

Your email address will not be published. Required fields are marked *

You can use the Markdown in the comment form.