2013-07-23 95 views
9

我从SO上发布的几个问题阅读了关于这一段的内容。为什么不保证memcpy对非POD类型安全?

我不能完全弄清楚为什么memcpy不能保证对非POD类型安全。我的理解是,memcpy只是一个按位拷贝。

下面是从标准

POD类型T的报价对于任何对象(比基类子对象等),对象是否保持T类型的有效值, 底层字节(1.7)构成的对象可以被拷贝到charunsigned char 0.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 
+3

如果定义了复制构造函数,而不是按位复制,会发生什么? –

+4

'memcpy'是一个按位拷贝。非POD类型不一定表现出按位复制语义。 – lapk

+0

但这不是标准所说的......标准说......“对象将随后保持其原始值”,这在记忆非POD类型时是正确的。 – ROTOGG

回答

6

试想拥有一些指针这样的缓冲类:

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它的实例之一,你会得到两个实例指向到同一个缓冲区。如果您在一个实例中修改数据,则会在另一个实例中修改数据。

这意味着你并没有真正将它克隆到两个独立的类中。而且,如果你删除了这两个类,那么缓冲区会从内存中释放两次,从而崩溃。 所以这个类必须定义一个拷贝构造函数,你必须使用构造函数来拷贝它。

+1

这实际上证明什么都没有。包含指针的结构会发生同样的情况,struct是POD类型。 – Dariusz

+2

@Dariusz在C++中'struct'就像'class',唯一的区别是'struct'默认为'public','class'为'private'。 –

+5

@Dariusz:只是因为某些东西是一个结构,并不会使它成为POD。我认为这篇文章试图做的一点与三条规则相同:dtor可以尝试删除数据指向的内存,当memcpyd导致双重删除时,拷贝策略解决的内存做一个深层复制。 – PlasmaHH

18

尝试按位复制std::shared_ptr<>。你可能会发现你的程序经常会在你的脸上爆炸。

你会遇到这个问题,任何类的复制构造函数做一个比特明智的副本以外的东西。在std::shared_ptr<>的情况下,它将复制指针,但不会增加引用计数,因此您最终将尽早释放共享对象及其引用计数,然后在复制的shared_ptr尝试减少释放时炸掉引用计数。


UPDATE:有人指出,这并不完全回答这个问题,这是公平的,因为我主要是针对抄袭的shared_ptr到shared_ptr的想法,shared_ptr不为char [],然后再返回。但是,这个原则依然成立。

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

POD也可能发生这种情况,但这是程序逻辑中的一个错误。只要程序能够理解并容纳这样的事件,按位拷贝回等同于修改过的shared_ptr的POD将是完全有效的。这样做对于std :: shared_ptr通常不起作用。

+0

我认为这并没有回答这个问题。我相信,在将shared_ptr的字节复制到char数组并立即返回之后,shared_ptr将具有其原始值。 OP不想对副本进行任何操作,而是将其复制回来,并想知道为什么这些来回等价语义只是为POD定义的。 (我可以想象垃圾收集方案可能会改变原始指针的值,从而使副本中的指针值无效,但除了可能与POD一起出现的MT问题之外,我认为我无法想象会出现什么问题。) –

+0

@ PeterA.Schneider我明白你的观点,但答案仍然成立。为了清晰起见,我已更新它。 –

1

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

+1

在给出的示例中,这不会成为问题,尝试恢复对象的原始值,而不是尝试创建具有相同值的第二个对象。注意这个例子中只有一个T对象。 –

+1

@ r-martinho-fernandes这不是真的。因为在两个memcpy调用之间“obj可能被修改”,这包括字符串可能被重新分配,包括释放不再使用的内存。在第二次赦免之后,原来的(现在无效的)指针将被恢复并等待破坏。 –

+0

@umläute在答案中提到! –

3

一般来说,问题是对象不仅引入数据,还引入行为

通过复制数据手工我们可以打破对象,它可以依靠拷贝构造函数的内在行为。

一个很好的例子是任何共享唯一指针 - 通过复制它,我们打破了“交易”,我们与一流的制作时,我们使用它。

不管复制过程是语义正确与否,这样做背后的想法是错误的,违反了对象的编程范式。

示例代码:

/** 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错误的析构函数)。

+0

在给出的例子中它会被打破吗? (不,它不会:在那里只有一个T对象,所以显然它的ID是唯一的) –

+0

@ R.MartinhoFernandes事实上它不会,但我想不出任何这样做。在该对象上没有明确的操作,并且在那之后发生的唯一的事情就是析构函数调用。 – Dariusz

+0

有“这两个调用之间std :: memcpy,obj可能会被修改” –

1

C++ 11 note:问题中的引用是规则的一个相当旧的版本。由于C++ 11,要求是平凡能够复制POD弱得多。


memcpy可以从任何对象使用。你得到一个对象的按位图像。

如果对象不是POD,则图像不能使用,好像它是同一类型与原始对象,这是因为寿命规则要求初始化首先完成。

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

+0

你可以构造一个具体的例子,来回地往返char数组使对象失效或导致UB? (假设单线程且没有中间代码。) –

+0

@ PeterA.Schneider:您提到的是一个更具限制性的场景。尽管如此,从完全或部分为“const”的对象中读取和写回,会产生未定义的行为,并可能在实际实现中导致硬件异常。 –