【问题标题】:Copy Arbitrary Type in C Without Dynamic Memory Allocation在没有动态内存分配的情况下复制 C 中的任意类型
【发布时间】:2015-12-08 20:22:17
【问题描述】:

问题:

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

/* 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 一直到现代的东西都应该很好),如果不是,是否可以修复以及如何修复?
  • 为了保持符合标准,此方法对自身施加了哪些使用限制?
    • 例如,据我了解,只要我从不直接使用 char-array 临时副本,就可以避免对齐问题 - 就像使用 memcpy 保存和加载的 bin 一样。但是我不能将这些地址传递给其他函数,期望指向我正在使用的类型的指针,而不会冒对齐问题的风险(显然在语法上我可以通过首先从char * 获取void * 来反常地做到这一点,甚至没有指定我正在使用的确切类型,但重点是我认为这样做会触发未定义的行为)。
  • 是否有更简洁和/或性能更*的方法来实现相同的目标?

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

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

理由:

这就是我认为上述方法有效的原因,以防你发现知道我来自哪里有助于解释我可能犯的任何错误:

C 标准的“字节”(传统意义上的“最小可寻址内存单元”,而不是现代化的“8 位”含义)是 char 类型 - sizeof 运算符以单位为单位生成数字char。因此,我们可以通过在该变量上使用 sizeof 运算符来获得任意变量类型所需的最小存储空间(我们可以在 C 中使用)。

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

就语法而言,给定类型数组的“名称”和指向同一类型的指针基本上可以同等对待。

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

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

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

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

由于我们不能显式使用/声明我们刚刚复制的变量的类型,并且由于某些架构可能对各种类型有对齐要求,这种 hack 有时会违反,我们不能直接使用这个副本- 我们必须 memcpy 将它返回到我们从中获取它的变量或相同类型的变量中,以便使用它。但是一旦我们把它复制回来,我们就有了我们最初放在那里的内容的精确副本。本质上,我们正在释放变量本身以用作暂存空间。

动机(或,“天哪!?!”):

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

当我这样做时,我遇到了需要额外的临时空间变量的情况,但是,由于缺少可移植的 typeof() 运算符,我无法在其中声明任何匹配类型的临时变量这样的“通用宏”sn-ps 代码。这是我发现的最接近真正便携的解决方案。

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

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

【问题讨论】:

  • 这可能行不通,因为数组会衰减为指针。
  • @BasileStarynkevitch:在这种情况下适用于什么地方?
  • 如果您需要编写如此糟糕的通用代码,认真考虑在其中添加一点 C++。通常我完全赞成使用纯 C 或现代 C++,但是当这导致一些令人费解的事情时......

标签: c arrays pointers language-lawyer memcpy


【解决方案1】:

是的,它有效。是的,它是 C89 标准。是的,很复杂。

小改进

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

如果速度很重要(因为如果这种操作很少见,它可能不重要),您可能更喜欢使用 intlong longsize_t 单位来对齐表格。

主要限制

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

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

由于我们不能显式使用/声明我们刚刚复制的变量的类型

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

如果你知道,你可以这样做:

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

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

【讨论】:

  • 是的,类型在原则上总是已知的:其动机是能够创建类函数的通用宏,它不需要使用宏的代码来显式指定类型。换句话说,对于编译器确实知道类型但语言没有标准的、更简洁的方法来利用这些知识来编写你想要的算法的情况,这是一个很大的解决方法。类型不可知的方式。简而言之,这是一种解决方法,因为没有标准化的 typeof 运算符。
  • 好的,所以你想把你的表达式封装成一个宏,其中的参数没有定义类型。而且你不能依赖 C++11 的特性,比如 auto,也不能依赖 gcc-only 的扩展,比如 typeof。那么它似乎更有意义。 Appart 关于对齐的段落,根据您的用例,这甚至可能无关紧要,我认为没有什么可以添加到您的提案中,这将正常工作。
  • 确实是通用宏,例如unsignedIntegralAddOverflows_m(a, b, overflows, result) 可以重写为例如unsignedIntegralAddOverflows_m(type, a, b, overflows, result)。为了与不需要知道类型的宏保持一致(因为它们不需要这样的“模板化”临时变量),我发现自己想要避免使用后者。但是,我承认,鉴于这个问题中 hack 的复杂性,让宏将类型作为参数开始变得更具吸引力。
【解决方案2】:

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

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

【讨论】:

  • 我同意这是一个问题,但据我所知,这将与指针的任何副本有关,无论是这种黑客方式还是正常方式。如果指针被释放或以其他方式无效,则在任何一种情况下都会遇到相同的问题。
  • 如果你使用特定类型的复制函数,你可以让它做一个“深度复制”,即对于任何指针成员,将指向的数据复制到一个单独的位置。那么你的新对象就独立于旧对象了,就没有我描述的问题了。
  • 好的,我明白你现在在说什么:如果所需的通用代码需要深度复制,问题中的技巧/黑客将无济于事。我认为这是“自然地”成为一个限制,但没有考虑明确说明。我绝对可以看到您的回答(+澄清 cmets)引起人们注意的价值,以防其他人没有想到这一点。赞成。
  • @arh:有时可以很好地工作的一个技巧是使用具有相同名称的 char[] 元素的各种联合类型,并让类似函数的参数访问传入的元素采用char[] 的参数和调用函数。不幸的是,即使所有内容都是 16 位或 32 位的倍数,别名规则也会阻止在可移植代码中使用更大的类型。
猜你喜欢
  • 1970-01-01
  • 2021-08-28
  • 1970-01-01
  • 2013-06-02
  • 2017-05-31
  • 2019-06-07
  • 2021-08-11
  • 2021-06-11
  • 1970-01-01
相关资源
最近更新 更多