如何在没有中间副本的情况下在标准C中实现memmove?

时间:2010-10-26 11:49:05

标签: c c99 undefined-behavior unspecified-behavior

从我系统的手册页:

  

void * memmove(void * dst,const void * src,size_t len);

     

说明
          memmove()函数将字符串src中的len个字节复制到字符串dst           两个字符串可能重叠;副本总是以非破坏性的方式完成           方式。

来自C99标准:

  

6.5.8.5当比较两个指针时,结果取决于   地址中的相对位置   指向的空间。如果   两个指向对象或不完整的指针   类型都指向同一个对象,   或者两者都指向过去的一个   同一个数组对象的元素,   他们相等。如果是对象   指向是同一成员   聚合对象,指向   结构成员后来宣布   比指针大   成员在早些时候宣布   结构和指向数组的指针   具有较大下标值的元素   比指针大   具有较低的相同数组的元素   下标值。所有指针   同一个union对象的成员   比较平等。如果表达式P   指向数组的元素   对象和Q指向的表达式   同一个数组的最后一个元素   对象,指针表达式Q+1   比较大于P。在所有   其他情况下,行为是   的未定义

重点是我的。

参数dstsrc可以转换为指向char的指针,以减轻严格的别名问题,但是有可能比较可能指向不同块内的两个指针,以便在它们指向同一个区块的情况下以正确的顺序进行复制?

显而易见的解决方案是if (src < dst),但如果srcdst指向不同的块,那么这是未定义的。 “未定义”意味着您甚至不应该假设条件返回0或1(这在标准词汇表中称为“未指定”)。

另一种选择是if ((uintptr_t)src < (uintptr_t)dst),至少是未指定的,但我不确定该标准是否保证在定义src < dst时,它等同于(uintptr_t)src < (uintptr_t)dst)。指针比较是从指针算法定义的。例如,当我在添加时阅读第6.5.6节时,在我看来,指针算法可以朝着与uintptr_t算法相反的方向运行,也就是说,当p时,兼容的编译器可能会有属于char*类型:

((uintptr_t)p)+1==((uintptr_t)(p-1)

这只是一个例子。一般来说,将指针转换为整数时,似乎可以保证很少。

这是一个纯粹的学术问题,因为memmove与编译器一起提供。在实践中,编译器作者可以简单地将未定义的指针比较提升为未指定的行为,或者使用相关的pragma强制其编译器正确编译它们的memmove。例如,this implementation有此代码段:

if ((uintptr_t)dst < (uintptr_t)src) {
            /*
             * As author/maintainer of libc, take advantage of the
             * fact that we know memcpy copies forwards.
             */
            return memcpy(dst, src, len);
    }

我仍然希望使用这个例子来证明标准对于未定义的行为过于宽泛,如果确实memmove无法在标准C中有效实现。例如,在回答时没有人勾选this SO question

5 个答案:

答案 0 :(得分:19)

我认为你是对的,不可能在标准C中有效地实施memmove

我认为,测试区域是否重叠的唯一真正可移植的方式是这样的:

[编辑:稍后看这个,我认为在第二行的末尾它应该是dst+len-1。但是我不能费心去测试它所以我现在就会离开,我可能第一次知道我在说什么。]

for (size_t l = 0; l < len; ++l) {
    if (src + l == dst) || (src + l == dst + len) {
      // they overlap, so now we can use comparison,
      // and copy forwards or backwards as appropriate.
      ...
      return dst;
    }
}
// No overlap, doesn't matter which direction we copy
return memcpy(dst, src, len);

您无法在可移植代码中有效地实现<{1}}或memcpy所有 ,因为无论您做什么,特定于平台的实施都可能会对您产生影响。但便携式memmove至少看起来似乎有道理。

C ++引入了memcpy的指针特化,它被定义为适用于同一类型的任何两个指针。它理论上可能比std::less慢,但显然在非分段架构上它不是。

C没有这样的东西,所以在某种意义上,C ++标准同意你C没有足够的定义行为。但是,C ++需要<等等。你不想在不知道实现std::map(或类似的东西)的情况下实现std::map(或类似的东西),而不知道实现memmove。实施

答案 1 :(得分:7)

对于两个有效且重叠的存储区域,我相信您需要处于6.5.8.5的定义情况之一。也就是说,数组的两个区域,union,struct等。

其他情况未定义的原因是因为两个不同的对象可能甚至不在相同类型的内存中,具有相同类型的指针。在PC体系结构中,地址通常只是虚拟内存的32位地址,但C支持各种奇怪的体系结构,其中内存不是那样的。

C保留未定义的原因是在不需要定义情况时给编译器编写者留出余地。阅读6.5.8.5的方法是一个段落,它仔细地描述了C想要支持的架构,除非它在同一个对象中,否则指针比较没有意义。

此外,编译器提供memmove和memcpy的原因是它们有时使用专门的指令写入目标CPU的调优组件中。它们并不意味着能够以相同的效率在C中实现。

答案 2 :(得分:2)

对于初学者来说,C标准因这样的细节存在问题而臭名昭着。部分问题是因为C在多个平台上使用,标准试图足够抽象以涵盖所有当前和未来的平台(可能使用一些超出我们所见过的任何复杂内存布局)。为了让编译器编写者为目标平台“做正确的事”,存在许多未定义或特定于实现的行为。包括每个平台的详细信息将是不切实际的(并且不断过时);相反,C标准将其留给编译器编写者来记录在这些情况下发生的事情。 “未指定”行为仅表示C标准未指定发生的情况,不一定不能预测结果。如果您阅读目标平台和编译器的文档,结果通常仍然可以预测。

由于确定两个指针是指向同一个块,内存段还是地址空间取决于该平台的内存是如何布局的,因此规范没有定义进行该确定的方法。它假定编译器知道如何进行此确定。你引用的规范部分说指针比较的结果取决于指针的“地址空间中的相对位置”。请注意,“地址空间”在这里是单数。本节仅涉及同一地址空间中的指针;也就是说,可以直接比较的指针。如果指针位于不同的地址空间中,则结果未被C标准定义,而是由目标平台的要求定义。

memmove的情况下,实现者通常首先确定地址是否可直接比较。如果没有,则该功能的其余部分是特定于平台的。大多数情况下,处于不同的存储空间足以确保区域不重叠,并且函数变为memcpy。如果地址可以直接比较,那么它只是一个简单的字节复制过程,从第一个字节开始,向前或从最后一个字节开始向后(无论哪个都可以安全地复制数据而不会破坏任何东西)。

总而言之,C标准在故意未指定的地方留下了很多,因为它无法编写适用于任何目标平台的简单规则。但是,标准编写者可以更好地解释为什么某些内容未定义并使用更多描述性术语,如“依赖于架构”。

答案 3 :(得分:1)

这是另一个想法,但我不知道它是否正确。为了避免史蒂夫回答中的O(len)循环,可以将其放在#else的{​​{1}}子句中,并使用强制转换为#ifdef UINTPTR_MAX实现。假设只要偏移量对指针有效,uintptr_tunsigned char *的强制转换就会通过添加整数偏移进行通信,这使得指针比较得到了很好的定义。

我不确定这种可交换性是否由标准定义,但它是有意义的,因为即使只有指针的低位是实际的数字地址而高位是某种黑盒子它也能工作

答案 4 :(得分:0)

  

我仍然希望使用这个例子来证明标准对于未定义的行为过于宽泛,如果确实memmove无法在标准C中有效实现

但这不是证据。绝对没有办法保证你可以比较任意机器架构上的两个任意指针。这种指针比较的行为不能由C标准甚至编译器立法。我可以想象一台具有分段架构的机器可能会产生不同的结果,具体取决于段在RAM中的组织方式,甚至可能选择在比较指向不同段的指针时抛出异常。这就是行为“未定义”的原因。完全相同的机器上的完全相同的程序可能会在运行之间产生不同的结果。

如果所有内存块都是从同一地址分配的,则使用两个指针的关系选择是从头开始还是从头开始复制到开头的memmove()给出“解决方案”的常用方法是有效的空间。幸运的是,这通常是这种情况,尽管它不是在16位x86代码的时代。