在没有动态内存分配的情况下在 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 实现上工作,但为了以防万一我遗漏了什么,我想知道:
- 这是否完全符合 C 标准(我相信从 C89 到现代的东西,这应该一直很好),如果不符合,是否可以修复以及如何修复?
- 为了保持符合标准,此方法对自身施加了哪些使用限制?
- 例如,据我所知,只要我从不直接使用字符数组临时副本,我就不会出现对齐问题——就像使用 memcpy 保存和加载的 bin 一样。但是我无法将这些地址传递给其他函数,这些函数期望指向我正在使用的类型的指针,而不冒对齐问题的风险(显然在语法上我可以通过首先从
char *
获取 void *
,甚至没有指定我正在使用的确切类型,但关键是我认为这样做会触发未定义的行为)。
- 是否有更简洁的 and/or 高效* 方法来实现相同的目的?
*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,这可能会导致复制性能不佳。
如果速度很重要(因为如果此操作很少见则可能不会),您可能更愿意使用 int
、long long
或 size_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 中进行泛型编程。对于小例子来说还可以,但不能很好地扩展。当您使用这些技术时,编译器无法为您捕获错误。
问题:
我想我已经找到了一种方法,据我所知,它允许您编写完全与类型无关的代码,该代码在 "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 实现上工作,但为了以防万一我遗漏了什么,我想知道:
- 这是否完全符合 C 标准(我相信从 C89 到现代的东西,这应该一直很好),如果不符合,是否可以修复以及如何修复?
- 为了保持符合标准,此方法对自身施加了哪些使用限制?
- 例如,据我所知,只要我从不直接使用字符数组临时副本,我就不会出现对齐问题——就像使用 memcpy 保存和加载的 bin 一样。但是我无法将这些地址传递给其他函数,这些函数期望指向我正在使用的类型的指针,而不冒对齐问题的风险(显然在语法上我可以通过首先从
char *
获取void *
,甚至没有指定我正在使用的确切类型,但关键是我认为这样做会触发未定义的行为)。
- 例如,据我所知,只要我从不直接使用字符数组临时副本,我就不会出现对齐问题——就像使用 memcpy 保存和加载的 bin 一样。但是我无法将这些地址传递给其他函数,这些函数期望指向我正在使用的类型的指针,而不冒对齐问题的风险(显然在语法上我可以通过首先从
- 是否有更简洁的 and/or 高效* 方法来实现相同的目的?
*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,这可能会导致复制性能不佳。
如果速度很重要(因为如果此操作很少见则可能不会),您可能更愿意使用 int
、long long
或 size_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 中进行泛型编程。对于小例子来说还可以,但不能很好地扩展。当您使用这些技术时,编译器无法为您捕获错误。