如何解决让“复制和交换习语”在这里工作的正确方法是什么?
我之前的问题:
在下面的代码中,我需要变量 auto ptr
到 remain valid
和 assertion
来传递。
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 中,应该在其析构函数中清除。这是异常安全的。
但是,这取决于两件你没有做的关键事情(正如评论中部分指出的那样):
-
一个清理旧状态的非抛出析构函数。您忽略了这一点,这对于正确管理此对象拥有的资源至关重要。
~数据() { 删除 [] local_data; }
注意:不需要设置为nullptr,也不需要检查nullptr,因为删除空指针是noop,一旦析构函数开始运行,对象就不存在了(生命周期已结束),因此不应再次读取它,否则您的程序有未定义的行为。
- 您没有编写复制构造函数。
当您没有编写正确的复制构造函数时,编译器会为您生成一个进行元素复制的构造函数。这意味着您最终会得到一个指针的副本,而不是它指向的数据的副本!这是一个别名错误,因为两个对象都将指向(并且在逻辑上“拥有”)相同的内存。无论哪个先被破坏,都会删除内存并破坏另一个仍然指向的内存。幸运的是,为您的类制作复制构造函数很容易:
Data(const Data& other) :
local_data{new char[other._size]}
_size{other._size},{
std::copy(other.local_data,other.local_data + _size,local_data);
}
关于这个复制构造函数的注意事项:
- 如果 new[] 抛出,则不会泄漏任何内容。 copy() 不能抛出。这是异常安全的。
- 初始化的顺序不是在构造函数中列出的顺序,而是在类中声明数据成员的顺序。因此,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 举报,一经查实,本站将立刻删除。