【问题标题】:What would a proper way to go, to get the "Copy & Swap Idiom" to work here?什么是正确的方法,让“复制和交换成语”在这里工作?
【发布时间】:2020-12-29 09:59:25
【问题描述】:

我的上一个问题:


在下面的代码中,我需要变量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++ String 类中运行得非常好。

#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;
}

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

【问题讨论】:

  • 指针仍然有效,因为你的类没有适当的析构函数并且"Some data" 永远不会被清理。您的示例在几个方面令人困惑。为什么operator= 执行交换?此外,ptr 只是a.local_data 的副本。改变一个对另一个没有任何影响。它们是指向相同数据的独立指针。
  • 但是,在这种情况下它确实发生了变化。我一步一步地检查了 Visual Studio IDE,观察了内容及其内存地址。在此处查看运行示例:onlinegdb.com/HybpyOO6P
  • ptr 仍然有效。 a.local_data 更改。它们不再指向相同的数据。由于您正在使用临时对象执行交换,因此您的示例也会崩溃,因此 a 不再有效。
  • @AlbinM 此外,不能保证断言不会在标准字符串类中失败。如果新文本很长(比如 100 个字符),它很可能会失败。这是一种实现定义的行为,在这种情况下断言会通过或失败。
  • 顺便说一句,您的length 函数将失败并返回一个空字符串。

标签: c++ c++11


【解决方案1】:

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

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

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

然而,这取决于两个您未能做到的关键事情(正如 cmets 部分指出的那样):

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

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

注意:你不需要将它设置为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 assignment 中受益,因为从右值复制可以得到优化,因为我们知道临时值即将被当分配完成时销毁,我们可以“窃取”它的分配,而不必创建我们自己的分配。这很快,而且异常安全:

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 持有的 old 地址,在您到达断言时该地址已被删除。在修改这些对象时存储指向对象内部的指针是出错的基本方法之一。

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

【讨论】:

  • 感谢您提供非常详细的回答。非常感谢。所以,如果我从你的回答中理解了你的意思,Data&amp; operator=(const Data&amp; data)
  • 你要搬什么?我认为答案是否定的,你不能在传统的复制构造函数中移动,因为源对象是一个左值(它是 const,这也表明它是禁区)。离开它就像在它还活着的时候把它的心撕掉,就像在末日神殿中一样。
  • 但是,移动和复制的行为是一样的。而且我指的不是传统的“复制构造函数”,而是“复制赋值运算符”和当前函数。
  • 哈哈哈,这主要是我自己的困惑,当时我下定决心尝试优化和改进代码。 .....哈哈哈哈,谢谢老兄!
猜你喜欢
  • 2021-08-14
  • 1970-01-01
  • 2014-08-05
  • 2017-09-09
  • 2018-01-28
  • 2021-04-02
  • 1970-01-01
  • 1970-01-01
  • 2011-01-03
相关资源
最近更新 更多