微信公众号搜"智元新知"关注
微信扫一扫可直接关注哦!

让“复制和交换习语”在这里工作的正确方法是什么?

如何解决让“复制和交换习语”在这里工作的正确方法是什么?

我之前的问题:


在下面的代码中,我需要变量 auto ptrremain validassertion 来传递。

   auto ptr = a.data();

看起来像这样:

   +--------------+
   | a.local_data | --\
   +--------------+    \     +-------------+
                        >--> | "Some data" |
   +-----+             /     +-------------+
   | ptr | -----------/
   +-----+

#include <iostream>
#include <cassert>
using namespace std;

class Data 
{
private:
    char* local_data;
    int _size = 0;
    
    inline int length(const char* str)
    {
        int n = 0;
        while(str[++n] != '\0');
        return n;
    }
    
public:
    Data() {
        local_data = new char[_size];
    }
    
    Data(const char* cdata) : _size { length(cdata) }{
        local_data = new char[_size];
        std::copy(cdata,cdata + _size,local_data);
    }
    
    int size() const { return _size; }
    const char* data() const { return local_data; }
    
    void swap(Data& rhs) noexcept
    {
        std::swap(_size,rhs._size);
        std::swap(local_data,rhs.local_data);
    }
    
    Data& operator=(const Data& data)
    {
        Data tmp(data);
        swap(tmp);
        return *this;
    }
};



int main()
{
    Data a("Some data");
    auto ptr = a.data(); // Obtains a pointer to the original location
    a = Data("New data");
    assert(ptr == a.data()); // Fails
    return 0;
}

编辑:为了提供一些观点,以下内容与标准 C++ 字符串类运行得非常好。

#include <iostream>
#include <string>
#include <cassert>

int main()
{
    std::string str("Hello");
    auto ptr = str.data();
    str = std::string("Bye!");
    assert(ptr == str.data());

    std::cin.get();
    return 0;
}

而且,我正在尝试实现相同的功能

解决方法

就正确性而言,与某些评论所表明的相反,您的赋值运算符对于复制/交换看起来是正确的:

Data& operator=(const Data& data)
{
    // Locally this code is fine
    Data tmp(data);
    swap(tmp);
    return *this;
}

它将数据复制到 tmp 中,并与之交换。因此,当前对象的新状态是数据的副本,而对象的旧状态在 tmp 中,应该在其析构函数中清除。这是异常安全的。

但是,这取决于两件你没有做的关键事情(正如评论中部分指出的那样):

  1. 一个清理旧状态的非抛出析构函数。您忽略了这一点,这对于正确管理此对象拥有的资源至关重要。

    ~数据() { 删除 [] local_data; }

注意:不需要设置为nullptr,也不需要检查nullptr,因为删除空指针是noop,一旦析构函数开始运行,对象就不存在了(生命周期已结束),因此不应再次读取它,否则您的程序有未定义的行为。

  1. 您没有编写复制构造函数。

当您没有编写正确的复制构造函数时,编译器会为您生成一个进行元素复制的构造函数。这意味着您最终会得到一个指针的副本,而不是它指向的数据的副本!这是一个别名错误,因为两个对象都将指向(并且在逻辑上“拥有”)相同的内存。无论哪个先被破坏,都会删除内存并破坏另一个仍然指向的内存。幸运的是,为您的类制作复制构造函数很容易:

Data(const Data& other) : 
    local_data{new char[other._size]}
    _size{other._size},{
    std::copy(other.local_data,other.local_data + _size,local_data);
}

关于这个复制构造函数的注意事项:

  1. 如果 new[] 抛出,则不会泄漏任何内容。 copy() 不能抛出。这是异常安全的。
  2. 初始化的顺序不是在构造函数中列出的顺序,而是在类中声明数据成员的顺序。因此,local_data 将在 _size 之前初始化,因此对 new 表达式使用 other._size 很重要。

复制/交换习语干净、简洁,可以导致异常安全的代码。但是,它确实有一些开销,因为它会将一个额外的对象放在一边,并完成与它交换的工作。这个习惯用法的好处是当多个操作可以抛出异常,而你想要一个“全有或全无”的赋值。在您的特定类中,唯一可以抛出的是 operator= 中 local_data 的分配,因此实际上没有必要在此类中使用此习语。

我觉得你的代码加入这些功能后应该没问题。在这种情况下,您也将从 move 构造函数move 赋值 中受益,因为可以优化从右值复制,因为我们知道临时对象即将分配完成后销毁,我们可以“窃取”它的分配,而不必创建我们自己的分配。这很快,而且异常安全:

Data(Data&& other) : 
    local_data{other._local_data}
    _size{other._size},{
    // important!  This prevents other's destructor from
    // deleting the allocation we just pilfered from it.
    // Note,other's size and pointer are inconsistent,but it's
    // about to be destroyed,so it doesn't matter.  If it did,// then swap both members,but that's needless more work
    // in this case. 
    other._local_data = nullptr;
}

Data& operator=(Data&& other) {
    _size = other._size;
    swap(local_data,other.local_data);
    return *this; 
}   

[已更新以解决此问题] 至于你的 main() 函数,断言看起来不合理。

int main()
{
    Data a("Some data");
    auto ptr = a.data(); // Obtains a pointer to the original location
    a = Data("New data");
    assert(ptr == a.data()); // ????
    return 0;
}

分配给 a 后,指针应该不同,并且您应该断言这些指针不相同。但在这种情况下,ptr 将指向 a 持有的 地址,该地址在您到达断言时已被删除。在修改对象的同时存储指向对象内部的指针是出错的基本方法之一。

最后一件事:如果您编写 operator= 或自定义构造函数,则几乎总是需要自定义析构函数。总是把这三者一起看作是一种特殊的关系。这被称为“三法则”:如果你写了其中任何一个,你几乎肯定必须写所有。该规则被扩展为“五分法则”(c++11 之后)以包括 move 构造函数和 move 赋值。您应该仔细阅读这些规则,并始终将这些特殊的成员函数一起考虑。另一个需要考虑的(不是针对这个类,而是在一般的类设计中)是最好的,零规则。

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。