在没有动态内存分配的情况下在 C 中复制任意类型

Copy Arbitrary Type in C Without Dynamic Memory Allocation

问题:

我想我已经找到了一种方法,据我所知,它允许您编写完全与类型无关的代码,该代码在 "stack" 上复制任意类型的变量(在引号中因为 C 标准实际上并不要求有一个堆栈,所以我真正的意思是它是用本地范围内的自动存储 class 复制的)。在这里:

/* Save/duplicate thingToCopy */
char copyPtr[sizeof(thingToCopy)];
memcpy(copyPtr, &thingToCopy, sizeof(thingToCopy));

/* modify the thingToCopy variable to do some work; do NOT do operations directly on the data in copyPtr, that's just a "storage bin". */

/* Restore old value of thingToCopy */
memcpy(&thingToCopy, copyPtr, sizeof(thingToCopy));

根据我有限的测试,它可以工作,而且我可以说它应该可以在所有符合标准的 C 实现上工作,但为了以防万一我遗漏了什么,我想知道:

*GCC 4.6.1 在我的 armel v7 测试设备上,使用 -O3 优化,使用对临时变量的正常分配生成与常规代码相同的代码,但可能是我的测试用例足够简单以至于它是能够弄清楚,如果更广泛地使用这种技术,它会感到困惑。

作为额外的兴趣传递,我很好奇这是否会破坏大部分与 C 兼容的语言(我知道的是 C++、Objective-C、D,也许还有 C#,尽管提到了也欢迎其他人)。

理由:

这就是我认为上述方法有效的原因,如果您发现了解我的来源有助于解释我可能犯的任何错误:

C 标准的 "byte"(在传统意义上的 "smallest addressable unit of memory",而不是现代“8 位”的含义)是 char 类型 - sizeof运算符以 char 为单位生成数字。因此,我们可以通过对该变量使用 sizeof 运算符来获得任意变量类型所需的最小存储量(我们可以在 C 中使用)。

C 标准保证几乎所有指针类型都可以隐式转换为 void *(但如果它们的表示不同,则表示会发生变化(但顺便说一下,C 标准保证 void *char * 具有相同的表示))。

就语法而言,给定类型的数组的 "name" 和指向相同类型的指针基本上可以被相同地对待。

sizeof 运算符是在编译时计算出来的,因此我们可以 char foo[sizeof(bar)] 而不依赖于有效的不可移植的 VLA。

因此,我们应该能够声明一个 "chars" 的数组,这是容纳给定类型所需的最小大小。

因此我们应该能够将要复制的变量的地址和数组的名称传递给memcpy(据我了解,数组名称隐式用作char * 到数组的第一个元素)。由于任何指针都可以隐式转换为 void *(需要更改表示形式),因此这是可行的。

memcpy 应该按位复制我们正在复制到数组的变量。无论类型是什么,涉及的任何填充位等,sizeof 保证我们将获取构成该类型的所有位,包括填充。

由于我们不能明确地use/declare我们刚刚复制的变量的类型,并且由于某些体系结构可能对各种类型有对齐要求,这种 hack 有时可能会违反,所以我们不能直接使用这个副本 - 我们必须 memcpy 它回到我们从中获得它的变量,或者一个相同类型的变量,以便使用它。但是一旦我们把它复制回来,我们就得到了我们最初放在那里的内容的精确副本。本质上,我们正在释放变量本身以用作临时 space.

动机(或,"Dear God Why!?!"):

我喜欢在有用的时候编写与类型无关的代码,但我也喜欢用 C 编写代码,将两者结合起来主要归结为在类似函数的宏中编写通用代码(然后您可以重新声明类型-通过调用类函数宏的包装函数定义进行检查)。把它想象成 C 中非常粗糙的模板。

当我这样做时,我 运行 遇到了需要额外的临时变量 space 的情况,但是,由于缺少可移植的 typeof() 运算符,我不能在这样的 "generic macro" 代码片段中声明匹配类型的任何临时变量。这是我发现的最接近真正便携的解决方案。

因为我们可以多次执行此技巧(足够大的 char 数组我们可以容纳多个副本,或者多个 char 数组大到可以容纳一个),只要我们可以保持我们的 memcpy 调用和直接复制指针名称,它在功能上就像拥有任意数量的复制类型的临时变量,同时能够保持通用代码类型不可知。

P.S。为了稍微转移可能不可避免的判断雨,我想说我确实认识到这是非常令人费解的,而且我只会在实践中将其保留用于经过良好测试的库代码,在这些代码中它显着增加了实用性,而不是什么我会定期部署。

是的,它有效。是的,它是C89标准。是的,很绕。

小改进

table 个字节 char[] 可以从内存中的任何位置开始。 根据 thingToCopy 的内容和 CPU,这可能会导致复制性能不佳。

如果速度很重要(因为如果此操作很少见则可能不会),您可能更愿意使用 intlong longsize_t 对齐 table单位代替。

主要限制

只有当您知道 thingToCopy 的大小时,您的命题才有效。 这是一个主要问题:这意味着您的编译器需要知道编译类型的 thingToCopy 是什么(因此,它不能是 incomplete type)。

因此,下面这句话令人不安:

Since we can't explicitly use/declare the type of the variable we just copied

没办法。为了编译 char copyPtr[sizeof(thingToCopy)];,编译器 必须 知道 thingToCopy 是什么,因此它必须能够访问它的类型 !

如果你知道,你可以简单地做到:

thingToCopy_t save;
save = thingToCopy;
/* do some stuff with thingToCopy */
thingToCopy =  save;

读起来更清晰,从对齐的角度来看甚至更好。

在包含指针(指向 const 的 const 指针除外)的对象上使用您的代码是不好的。有人可能会修改指向的数据或指针本身(例如 realloc)。这会使您的对象副本处于意外甚至无效状态。

泛型编程是 C++ 背后的主要驱动力之一。其他人尝试使用宏和强制转换在 C 中进行泛型编程。对于小例子来说还可以,但不能很好地扩展。当您使用这些技术时,编译器无法为您捕获错误。