【问题标题】:Why isn't memcpy guaranteed to be safe for non-POD types?为什么不能保证 memcpy 对于非 POD 类型是安全的?
【发布时间】:2013-07-23 12:26:57
【问题描述】:

我从 SO 上发布的几个问题中了解到这一段。

我不太明白为什么不能保证memcpy 对于非 POD 类型是安全的。我的理解是memcpy 只是按位复制。

以下是标准的引用

对于POD类型T的任何对象(基类子对象除外),无论该对象是否拥有T类型的有效值, 构成对象的底层字节(1.7)可以复制到charunsigned char.41的数组中)如果内容 charunsigned char 的数组被复制回对象中,对象随后应保持其原始 价值。

# define N sizeof (T)
char buf[N];
T obj ; // obj initialized to its original value
std :: memcpy (buf , & obj , N); // between these two calls to std::memcpy,
                                 // obj might be modified
std :: memcpy (& obj , buf , N); // at this point, each subobject of obj of 
                                 // scalar type holds its original value

【问题讨论】:

  • 如果定义了一个复制构造函数而不是按位复制会发生什么?
  • memcpy 是按位复制。非 POD 类型不一定表现出按位复制语义。
  • @LeonLi 不是。原始值不是原始字节。
  • @R.M.F 我仍然很困惑......为什么在两个 memcpy 之后不能保留“原始值”。如果 memcpy 只是按位操作,那么在 memcpy 期间可能会发生什么变化。
  • 从中吸取的教训是,C++ 是一种高级语言,具有低级逃生舱口,而不是低级语言。停止考虑位和字节,开始考虑值、对象和数字。

标签: c++


【解决方案1】:

总的来说,问题在于对象不仅会引入数据,还会引入行为

通过手动复制数据,我们可能会破坏对象的固有行为,这可能依赖于复制构造函数。

一个很好的例子是任何 sharedunique 指针 - 通过复制它,我们破坏了我们在使用它时与该类达成的“交易”。

无论复制过程在语义上是否正确,这样做背后的想法都是错误的,并且违反了对象编程范式。

示例代码:

/** a simple object wrapper around a pthread_mutex
 */
class PThreadMutex
{
   public:
    /** locks the mutex. Will block if mutex is already locked */
    void lock();

    /** unlocks the mutex. undefined behavior if mutex is unlocked */
    void unlock();

   private:
    pthread_mutex_t m_mutex;

};

/** a simple implementation of scoped mutex lock. Acquires and locks a Mutex on creation,
 * unlocks on destruction
 */
class ScopedLock
{
  public:
    /** constructor specifying the mutex object pointer to lock
     * Locks immediately or blocks until lock is free and then locks
     * @param mutex the mutex pointer to lock
     */
    ScopedLock ( PThreadMutex* mutex );

    /** default destructor. Unlocks the mutex */
    ~ScopedLock ();

    /** locks the mutex. Will block if mutex is already locked */
    void unlock();


  private:

    PThreadMutex* m_mutex;

    // flag to determine whether the mutex is locked
    bool m_locked;

    // private copy constructor - disable copying
    ScopedLock(ScopedLock &mutex) { (void)mutex; /* to get rid of warning */ };

};

如果您复制ScopedLock 类,手动解锁它,然后恢复该值并在构造函数中执行另一次解锁,这将导致未定义的行为(或至少在析构函数中出现 EPERM 错误)。

【讨论】:

  • 在给出的例子中会被破坏吗? (不,它不会:那里只有一个 T 对象,所以显然它的 ID 是唯一的)
  • @R.MartinhoFernandes 确实不会,但我想不出任何会。没有对该对象进行显式操作,之后唯一发生的事情就是析构函数调用。
  • “在这两个对 std::memcpy 的调用之间,obj 可能会被修改”
【解决方案2】:

想象一个类,它持有一些指向缓冲区的指针,如下所示:

class Abc {
    public:
    int* data;
    size_t n;
    Abc(size_t n)
    {
        this->n = n;
        data = new int[n];
    }

    // copy constructor:
    Abc(const Abc& copy_from_me)
    {
        n = copy_from_me.n;
        data = new int[n];
        memcpy(data, copy_from_me.data, n*sizeof(int));
    }
    Abc& operator=(const Abc& copy_from_me)
    {
        n = copy_from_me.n;
        data = new int[n];
        memcpy(data, copy_from_me.data, n*sizeof(int));
        return *this;
    }

    ~Abc()
    {
        delete[] data;
    }
} ;

如果你只是 memcopy 它的一个构造实例,你会得到两个实例指向同一个缓冲区data,因为它们在data 指针中具有相同的缓冲区地址。如果您在一个实例中修改数据,它也会在另一个实例中被修改。

这意味着您并没有真正将它克隆到两个独立的类中。此外,如果您随后删除这两个类,缓冲区将从内存中释放两次,这将导致崩溃。 所以这个类必须定义一个复制构造函数,而你必须使用构造函数来复制它。

【讨论】:

  • 这实际上证明不了。包含指针的 struct 也会发生同样的情况,并且 struct 是 POD 类型。
  • @Dariusz 在 C++ 中struct 就像class,唯一的区别是struct 默认为publicclassprivate
  • @Dariusz:仅仅因为某些东西是一个结构,并不能使它成为一个 POD。我认为帖子试图提出的观点与三规则相同:dtor 可以尝试删除 data 指向的内存,当 memcpyd 会导致双重删除时,复制 ctor 解决了这个问题做一个深拷贝。
  • @PlasmaHH 我想我知道你们俩的意思,但答案并没有解释这一点,至少不是我第一次发表评论时的形式。目前显示的代码也没有显示问题。我认为答案必须扩大才能很好。
  • 这个新的例子也好不到哪里去。无论您是 memcpy 还是像Abc y = x; 这样的常规副本,它都会中断。损坏的不是内存,而是班级本身。坏代码当然坏了。
【解决方案3】:

尝试按位复制std::shared_ptr<>。你可能会发现你的程序经常在你面前崩溃。

任何类的拷贝构造函数做的不是按位拷贝,你都会遇到这个问题。在std::shared_ptr<> 的情况下,它会复制指针但不会增加引用计数,因此您最终会提前释放共享对象及其引用计数,然后在复制的shared_ptr 尝试时爆炸减少释放的引用计数。


更新: 有人指出,这并不能完全回答问题,这是公平的,因为我主要解决了将 shared_ptr 复制到 shared_ptr 的想法,而不是 shared_ptr 到 char[] 并返回再次。但是,这个原则仍然成立。

如果您按位将 shared_ptr 复制到 char[],为 shared_ptr 分配不同的值,然后将 char[] 复制回来,最终结果可能是泄漏一个对象并双重删除另一个对象,即, UB。

同样的情况也可能发生在 POD 上,但这将是程序逻辑中的一个错误。只要程序理解并适应这样的事件,按位复制回修改后的 shared_ptr 的等效 POD 将是完全有效的。对 std::shared_ptr 这样做通常是行不通的。

【讨论】:

  • 我认为这并不能回答问题。我相信在将 shared_ptr 的字节复制到 char 数组并(立即)返回后,shared_ptr 将具有其原始值。 OP 不想对副本做任何事情,而是将其复制回来,并想知道为什么这些来回等价语义只为 POD 定义。 (我可以想象垃圾收集方案可能会更改原始指针值,从而使副本中的指针值无效,但除了 POD 也可能出现的 MT 问题之外,我认为,我无法想象会出现什么问题。)
  • @PeterA.Schneider 我明白你的意思,但答案仍然成立。为了清楚起见,我已对其进行了更新。
【解决方案4】:

例如,假设您正在编写一个String 类。该类的任何实例都应包含指向某个动态分配的char 数组的指针。如果你 memcopy 这样一个实例,那么这两个指针将是相等的。对一个字符串的任何修改都会影响另一个字符串。

【讨论】:

  • 在给出的示例中不会出现问题,其中尝试恢复对象的原始值,而不是尝试创建具有相同值的第二个对象。请注意该示例如何只有一个 T 对象。
  • @r-martinho-fernandes 这不是真的。由于在两个 memcpy 调用之间“obj 可能被修改”,这包括字符串可能被重新分配,包括释放不再使用的内存。在第二次 mempcy 之后,原始(现在无效)指针将被恢复并等待破坏。
  • @umläute 在答案中提及!
【解决方案5】:

C++11 注意:问题中的引用是该规则的一个相当旧的版本。从 C++11 开始,要求是可轻松复制,这比 POD 弱得多。


memcpy 可以用于任何对象。您会得到对象的按位图像。

如果对象不是 POD,则不能将图像用作与原始对象相同类型的图像,因为生命周期规则需要先完成初始化。

在这种情况下,图像只是一堆字节。这可能仍然有用,例如检测对象内部表示随时间的变化,但只有对字节有效的操作(例如两个图像之间的比较)是合法的,而不是需要原始类型对象的操作。

【讨论】:

  • 你能构造一个具体的例子,在 char 数组中来回复制会使对象无效或导致 UB 吗? (假设单线程并且没有中间代码。)
  • @PeterA.Schneider:您正在命名一个更具限制性的场景。尽管如此,读取和写入完全或部分为const 的对象会产生未定义的行为,并可能在实际实现中导致硬件异常。
猜你喜欢
  • 1970-01-01
  • 2011-07-11
  • 1970-01-01
  • 2022-01-20
  • 2013-07-31
  • 1970-01-01
  • 2010-09-20
相关资源
最近更新 更多