【发布时间】:2015-12-22 17:09:38
【问题描述】:
考虑返回动态或自动数组。与 C 无关。
返回数组的常用技术是:A)被调用者在堆上分配并返回,B)调用者在堆栈上分配并传递给被调用者。
// A
void caller(void) {
int *a = callee();
free(a);
}
int *callee(void) {
int *a = malloc(10 * sizeof(*a));
return a;
}
// B
void caller(void) {
int a[10]; callee(a, sizeof(a) / sizeof(a[0]));
}
void callee(int *a, size_t n) {
//
}
情况 A 可能会导致不必要的 allocate-free 循环,而情况 B 需要调用者中的语法垃圾。在 B 中,我们也无法在被调用者中计算 n,因为它是预定义的。我们也不能返回自动存储,因为它会在返回时被销毁(访问它通常是 UB)。
但是,如果我们引入新的return_auto 运算符,该运算符将从被调用者返回,但保持其堆栈帧不变,就好像调用者在它自己的堆栈上完成了所有工作一样?
// C
void caller(void) {
int *a = callee();
}
int *callee() {
int a[compute_n()];
return_auto a;
}
我的意思是,调用者可以继承被调用者的堆栈框架,所有问题都消失了。在 return_auto 之后,它的堆栈帧如下所示:
[caller frame]
arguments
ret-pointer
locals
int *a = callee.a
[callee frame] (defunct)
arguments
ret-pointer
locals
int a[n] (still alive)
[end-of-callee-frame]
[end-of-caller-frame]
在机器代码(至少 x86)中,这可以通过跳转到 ss:ebp 而不是 mov esp, ebp / ret n 的 ret 指针来实现。我们已经有了现代 C 语言中的 VLA,这看起来非常相似,但稍微有点复杂。
当然应该小心使用,因为一系列的 return_auto 会在堆栈上留下相当大的转储,只有当最外面的调用者返回时(通常)才会“收集”。但是堆栈分配非常便宜,理论上一些算法可以从根本不调用 malloc/free 中受益。从代码结构的角度来看,这也很有趣,而不仅仅是性能。
有谁知道这种技术在哪里实现/堆栈帧加入? (这里C只是一个例子)
好的,需要一个简单的例子。
void caller(Context *ct) {
char *s = make_s(ct);
printf("%s\n", s);
}
void make_s(Context *ct) {
const char *tag = "?", *name = "*";
if (ct->use_tag) tag = ct->tag;
else if (ct->app) tag = ct->app->tag;
if (ct->app) name = ct->app->name;
char s[strlen(tag)+strlen(name)+10];
snprintf(s, len, "%s.object(%s)", name, tag);
return_auto s;
}
显然,现在我们需要在调用者的身体中分解它(可能通过宏来感受所有警告)或在被调用者中执行asprintf/malloc,在调用者中执行free。
【问题讨论】:
-
哇,我们使用动态内存分配已有 50 多年了,怎么可能所有的编程语言理论家都错过了这个显而易见的解决方案? :)
-
嗯,我不确定。你说“这种技术”,好像这个想法是有道理的;我质疑这种感觉。
-
int a[SIZE]; callee(a, SIZE); -
基本问题是,一旦一个函数像这样分配一个数组,你就不能在程序的整个其余部分从那里展开堆栈。它本质上违反了将堆栈用于临时的整个想法,因为现在堆栈框架成为永久的。
-
另一种看待它的方式是将堆栈变成堆。