为什么移动语义与动态内存分配中的浅拷贝具有相同的行为?

Why move semantics have the same behavior as shallow copy in dynamic mem allocation?

在处理动态mem分配的类中,浅拷贝基本上导致程序两次删除一个资源。在移动操作中, 原始指针不再指向资源,但是为什么在移动语义中会出现相同的行为呢?例如:

#include <utility>
#include <cstring>
using namespace std;
class MyString
{
    char* cstr;
 public:
    MyString(const char* arg)
    : cstr(new char[strlen(arg)+1])
    {
        strcpy(cstr, arg);
    }
    MyString(MyString&&) = default;
    MyString& operator=(MyString&&) = default;
    MyString(const MyString&) = delete;
    MyString& operator=(const MyString&) = delete;
    ~MyString()
    {
        delete[] cstr;
    }
};

int main()
{
    MyString S1{"aaa"};
    MyString S2 = move(S1); // error

}

我试过 3 种不同的编译器,得到了相同的结果。

隐式生成的移动构造函数移动构造每个成员。你的成员是一个指针。移动指针(或任何其他原始对象)与复制指针相同。

因此,由于您的移动构造函数除了移动指针外什么都不做,从 MyString 移动的对象仍将指向与移动到 MyString 的对象相同的指针对象。当两者都被销毁时,析构函数将尝试删除指针对象两次。

你需要遵循5的规则:如果需要实现析构函数、move/copy构造函数、move/copy赋值,那么它们很可能都需要实现(或删除) .您已经实现了析构函数来删除拥有的指针,因此您必须实现移动构造函数(和其他构造函数),以便从中移动(或从中复制)的对象不再指向它不再拥有的对象。

Why move semantics have the same behavior as shallow copy in dynamic mem allocation?

因为移动只是浅拷贝的代名词。移动构造函数和赋值运算符将被赋予自定义实现,以防需要清理从对象移出的对象以维护 class 不变性。就像复制构造函数和复制赋值运算符一样,可以进行不破坏 class 不变量的深度复制。

[why is] Moving a pointer (or any other primitive object) is same as copying it.

因为在 C++ 中,你不用为你不用的东西付费(一般来说)。

C++ 中的裸指针(又名:不是智能指针)被假定为不拥有它指向的对象。这就是为什么您必须手动删除它;编译器不会为超出范围的任何指针生成隐式 delete 语句。

既然指针不拥有内存,为什么在移动指针时清空指针的来源是正确的?它不是。对于非拥有指针的移动来说,复制指针值并保留旧指针值是完全有效的。就像在不破坏它指向的内容的情况下销毁非拥有指针是完全有效的。

C++ 不知道您需要删除该指针,就像它不知道您需要清空旧指针一样。如果您需要,那么 必须这样做。

如果你想创建一个拥有指针,那么你必须创建那些语义。这些语义比非拥有语义更昂贵。指针移动只是按位复制;智能指针移动必须 运行 实际代码。它必须执行两个内存存储操作:将旧值存储在新指针中,以及清空旧指针。比一个还贵

因此,您必须明确要求或自己做。


要了解对性能的潜在影响,请考虑 vector<T*>。现在,让我们考虑这个类型是 class 的私有成员。这个 class 动态分配了一些 T 并将它们放在 vector 中。它还确保在删除 class 时删除添加的任何 T

那么,让我们考虑一下 vector<T*> 会是什么样子,您提出的移动指针导致旧值变为 nullptr 的想法。 vector 的一个重要元素是重新分配;当您插入的元素超过 vector 可以容纳的数量时,它必须分配更大的存储空间并将所有元素移动到新存储空间,然后删除旧存储空间。

根据您提出的想法,重新分配 vector<T*> 意味着移动所有指针元素。但是每一步都是两个内存操作:复制值和清空旧值。但问题是,旧值?该值即将被 销毁 。而拥有 vector<T*> 的 class 不需要删除它或任何东西;它将在新的存储中。所以没有理由 vector<T*> 需要清空一个将被删除的值。

相比之下,现代 vector<T*> 实现将 "move" 通过将整个缓冲区的单个 memcpy 指针指向新位置。它可以做到这一点,因为指针是可简单复制的,这允许它们通过按字节复制来复制和移动。

所以你的方法慢很多。而且您的方法一无所获,因为在释放内存时旧值将被删除。所以在这种情况下没有必要将其取消。

C++ 不知道您是在实现 vector 还是在实现智能指针。因此,它适用于最低公分母:指针是可简单复制的。如果你想要专门的移动行为,你必须实现它。