【问题标题】:Safely moving a C++ object安全移动 C++ 对象
【发布时间】:2010-11-27 13:30:54
【问题描述】:

我听说过一些关于通过memcpy 将对象传送到另一个内存位置的警告,但我不知道具体原因。除非它包含的成员会根据内存位置做一些棘手的事情,否则这应该是完全安全的……或者不是?

编辑:预期的用例是像vector 这样的数据结构,它将对象(不是对象的指针)存储在连续的内存块(即数组)中。要在第 n 位置插入新对象,所有从位置 n 开始及以后的对象都需要移动,以便为插入的对象腾出空间。

【问题讨论】:

  • 我曾经实现了一个容器 (spotep.com/dev/devector.h),在那里我想到了同样的事情,但我还没有找到一个不能“移动”的类。我能想到的唯一情况是,如果某个类以某种奇怪的方式注册了自己的内存位置,或者由指向自身的指针组成。我听说有 std::string 实现可以指向它们自己或堆上,但我还没有真正看到它..
  • 如果移动内存不足的双向链表会崩溃。将节点中的指针与您来自的节点的地址进行异或的技巧将不起作用。
  • @Victor:在具有虚拟基类的 MI 场景中,如果运行时将对虚拟基类的引用存储为指针而不是偏移量,这可能会失败。
  • 我不会搞乱向量之类的内部结构。

标签: c++ memory-management pointers


【解决方案1】:

不应该这样做的一个主要原因是析构函数。当您将 C++ 对象 memcpy 到内存中的另一个位置时,您最终会在内存中得到该对象的 2 个版本,而该对象仅运行了 1 个构造函数。这将破坏几乎所有单个 C++ 类的资源释放逻辑。

【讨论】:

  • 是的,这是有道理的。但是,我不是在复制而是移动对象,使之前的位置无效(即不再使用任何指针引用它),因此不会在旧内存位置调用析构函数。
  • @Jen 除非你对析构函数将运行的旧对象在内存中的先前位置做了真正邪恶的事情。
  • 怎么样?调用析构函数的唯一方法是通过指针。如果没有指针指向内存位置,它只是一堆位。还是我错过了什么?
  • 具有自动存储期限的对象在超出范围时会自动调用其析构函数。
  • “除非你做一些真正邪恶的事情[]析构函数会运行” 如果它是一个自动变量或由另一个对象创建,当然,但如果它是明确的( O *p = new O;) 已分配。那么问题是 Jen 又回到了以程序员为中介的对象管理上,这带来了所有的风险。
【解决方案2】:

语言规范不允许。这是未定义的行为。也就是说,最终,它有什么问题。在实践中,它往往会与虚函数调用混淆,这意味着析构函数将运行两次(并且比构造函数更频繁),成员对象是浅拷贝的(因此,例如,如果您尝试使用 @ 987654321@,它爆炸了,因为多个对象最终指向同一个内部数组。)

POD 类型除外。它们没有(复制)构造函数、析构函数、虚函数、基类或任何其他可能导致中断的东西,因此,您可以使用 memcpy 复制它们。

【讨论】:

    【解决方案3】:

    为了便于讨论,我假设您的意思是指原始对象“丢弃”(不再使用,没有运行它的析构函数)而不是有两个副本(这会导致更多问题,引用计数关闭等)。我通常指的是能够做到这一点的属性是按位可移动的。

    在我处理的代码库中,大多数对象都是按位可移动的,因为它们不存储自引用。但是,某些数据结构不是按位可移动的(我相信 gcc 的 std::set 不是按位可移动的;其他示例是 linked list nodes)。一般来说,我会避免尝试使用这个属性,因为它会导致一些非常难以调试的错误,并且更喜欢面向对象调用复制构造函数。

    编辑添加

    对于某人如何/为什么这样做似乎有些困惑:这是我对如何做的评论:

    通常情况下,我看到上面的替代 向量的实现。记忆 通过分配 malloc(sizeof(Class)*size) 和 对象是通过 显式调用构造函数和 析构函数。有时(比如在 重新分配)它们必须被移动, 所以选择是做 std::vector 的 重复调用复制构造函数 关于新的内存和析构函数 旧的,或者使用 memcopy 并且只是“免费” 旧街区。大多数时候是后者 只是“有效”,但并不适合所有人 对象。

    至于为什么,memcopy(或realloc)方法可以明显更快。

    是的,它会调用未定义的行为,但它也往往适用于大多数对象。有些人认为速度值得。如果您真的打算使用这种方法,我建议您实现一个 bitwise_movable 类型特征以允许将其适用的类型列入白名单,并为不在白名单中的对象使用传统副本,就像示例 here 一样。

    【讨论】:

      【解决方案4】:

      如果对象中没有指针,也没有虚函数,也没有具有任何相同的子函数,那么您可能会侥幸成功。不推荐!!!

      这应该使用复制或深度复制函数或重写的运算符来完成。

      在该方法中,您将调用一个新的构造函数并将其包含的数据项一一复制。

      对于浅拷贝,您将复制指针/引用,因此您将拥有两个指向相同包含元素的对象......潜在的内存泄漏噩梦。

      对于深层副本,您将遍历包含的对象和引用,同时创建它们的新副本。

      要移动对象,您需要复制它并删除原始对象。

      【讨论】:

        【解决方案5】:

        简答:std::memcpy() 用于移动内存,而不用于移动对象。尽管如此,使用它会调用未定义的行为。

        稍微长一点的答案:不是 POD 的 C++ 对象可能包含需要释放的资源,并且这些资源保存在无法轻松复制的句柄中。 (一种流行的资源是内存,其中句柄是一个指针。)它还可能包含由实现插入的东西(虚拟基类实例指针),不应像内存一样被复制。

        在 C++98 和 C++03 中移动对象的唯一正确方法是将其复制构造到新位置并在旧位置调用析构函数。 (在 C++1x 中会有移动语义,所以在某些情况下事情可能会变得更有趣。)

        【讨论】:

        • C++1x 中的移动语义对此没有任何影响。它用于防止创建/销毁临时对象的情况。将此调用 foo("Hello") 带到此函数 foo(const std::string& s)。如果函数 foo(const std::string&& s) 存在,移动语义将减少创建 s 临时变量的需要。
        • @jmucchiello:这就是为什么我用“......在某些情况下”来限定我的陈述。 (顺便说一句,我不认为你的例子是正确的。在这两种情况下都需要创建一个临时的。)
        【解决方案6】:

        在我的脑海中:如果你只是做一个 memcpy,你最终会做一个浅拷贝。如果你需要一个深拷贝,那么这是行不通的。

        复制构造函数和赋值运算符到底有什么问题?

        【讨论】:

          【解决方案7】:

          通常(在所有语言中,不仅仅是 C++),为了安全地移动对象,您还需要重写对该对象的所有指针/引用以指向新位置。这是 C++ 中的一个问题,因为没有简单的方法可以判断系统中的任何对象是否具有指向您正在移动的对象的“隐藏”指针。正如您所指出的,某些类可能包含指向自身的隐藏指针。其他类可能在跟踪所有实例的工厂对象中具有隐藏指针。看似不相关的类也有可能出于各种原因缓存指向对象的指针。

          唯一安全的方法是如果您对系统中的所有对象具有某种反射访问权限,以便您可以找到指向该对象的所有指针并重写它们。在任何情况下,这都是一个潜在的非常昂贵的操作,因此需要它的系统(例如复制垃圾收集器)往往会被非常仔细地组织以一次复制许多对象和/或绑定需要搜索的位置带有写屏障等的指针。

          【讨论】:

            猜你喜欢
            • 2016-02-08
            • 1970-01-01
            • 2018-11-29
            • 1970-01-01
            • 2011-04-30
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            相关资源
            最近更新 更多