3

const考虑以下代码,它以“嵌套”方式将临时对象绑定到引用:

#include <iostream>

std::string foo()
{
    return "abc";
}

std::string goo()
{
    const std::string & a = foo();
    return a;
}

int main()
{
    // Is a temporary allocated on the heap to support this, even for a moment?
    const std::string & b = goo();
}

我一直试图了解编译器必须在内存存储方面做什么才能支持这种“嵌套”结构。

我怀疑对于对 的调用foo(),内存分配很简单:std::string当函数foo()退出时,将在堆栈上分配 a 的存储空间。

但是,编译器必须做些什么来支持对 引用的对象的存储b?函数的堆栈goo必须展开并“替换为”所b引用的堆栈上的对象,但是为了展开堆栈goo,编译器是否需要暂时在堆上创建对象的副本(在复制之前)它回到不同位置的堆栈)?

或者编译器是否有可能在没有在堆上分配任何存储的情况下完成这个构造的要求,即使是片刻?

或者,编译器甚至可以为 引用的对象和 引用的对象使用相同存储位置,而无需在堆栈或堆上进行任何额外的分配?ba

4

5 回答 5

7

我认为您没有考虑中间步骤,即您没有绑定ba,而是绑定到a. 这不是由于任何花哨的记忆恶作剧!

goo按值返回,因此,根据所有常用机制,该值在内部完整表达式的范围内可用。main它要么在main的堆栈框架中,要么在其他地方,或者(在这种人为的情况下)可能完全优化。

这里唯一的魔力是它一直保持main' 范围内,直到b超出范围,因为b它是一个 ref-to- const(而不是几乎立即被销毁)。

那么,堆会以任何方式进入它吗?好吧,如果你有一个堆,没有。如果您的意思是免费商店,那么,仍然没有。

于 2012-12-05T19:18:08.520 回答
4

从理论上讲,由于goo(并且foo就此而言)按值返回,a因此将返回引用的变量的副本(并放置在堆栈上)。所述副本的生命周期将延长b, 直到b' 范围结束。

我认为您缺少的要点是您按值返回。这意味着在foogoo返回之后,它们里面的任何东西都没有区别——你留下了一个绑定到const引用的临时字符串。

在实践中,一切都很有可能被优化。

于 2012-12-05T19:15:58.100 回答
4

以下是 C++ 标准允许编译器重建代码的示例。我正在使用完整的 NRVO。请注意放置的使用new,这是一个比较晦涩的 C++ 功能。您传递new一个指针,它在那里而不是在免费存储中构造结果。

#include <iostream>

void __foo(void* __construct_std_string_at)
{
  new(__construct_std_string_at)std::string("abc");
}

void __goo(void* __construct_std_string_at)
{
  __foo(__construct_std_string_at);
}

int main()
{
  unsigned char __buff[sizeof(std::string)];
  // Is a temporary allocated on the heap to support this, even for a moment?
  __goo(&__buff[0]);
  const std::string & b = *reinterpret_cast<std::string*>(&__buff[0]);
  // ... more code here using b I assume
  // end of scope destructor:
  reinterpret_cast<std::string*>(&__buff[0])->~std::string();
}

如果我们阻止了 NRVO goo,它看起来会像

#include <iostream>

void __foo(void* __construct_std_string_at)
{
  new(__construct_std_string_at)std::string("abc");
}

void __goo(void* __construct_std_string_at)
{
  unsigned char __buff[sizeof(std::string)];
  __foo(&__buff[0]);
  std::string & a = *reinterpret_cast<std::string*>(&__buff[0]);
  new(__construct_std_string_at)std::string(a);
  // end of scope destructor:
  reinterpret_cast<std::string*>(&__buff[0])->~std::string();
}

int main()
{
  unsigned char __buff[sizeof(std::string)];
  // Is a temporary allocated on the heap to support this, even for a moment?
  __goo(&__buff[0]);
  const std::string & b = *reinterpret_cast<std::string*>(&__buff[0]);
  // ... more code here using b I assume
  // end of scope destructor:
  reinterpret_cast<std::string*>(&__buff[0])->~std::string();
}

基本上,编译器知道引用的生命周期。因此它可以创建“匿名变量”来存储变量的实际实例,然后创建对它的引用。

我还注意到,当你调用一个函数时,你实际上(隐式地)传递了一个指向返回值所在的缓冲区的指针。因此,被调用函数在调用者的范围内“就地”构造对象。

使用NRVO,被调用函数范围内的命名变量实际上是在调用函数“返回值所在的地方”构造的,这使得返回变得容易。没有它,您必须在本地完成所有操作,然后在 return 语句中将您的返回值复制到通过相当于放置 new 的隐式指针指向您的返回值。

不需要在堆上做任何事情(又名免费存储),因为生命周期都很容易证明和堆栈排序。

原始的foogoo具有预期签名的必须仍然存在,因为它们具有外部链接,直到发现没有人使用它们时可能被丢弃。

所有以 开头的变量和函数__仅用于说明。编译器/执行环境不再需要命名变量,就像您需要为红细胞命名一样。(理论上,因为__是保留的,所以在编译之前进行这种翻译传递的编译器可能是合法的,如果您实际使用了这些变量名并且它未能编译,那将是您的错,而不是编译器的错,而是......那将是一个非常老套的编译器。;))

于 2012-12-05T19:30:21.923 回答
3

不,不会对生命周期延长进行任何动态分配。常见的实现等价于下面的代码转换:

std::string goo()
{
    std::string __compiler_generated_tmp = foo();
    const std::string & a = __compiler_generated_tmp;
    return a;
}

不需要动态分配,因为只要引用还活着,生命周期就会被延长,并且按照在当前范围结束时发生的 C++ 生命周期规则。通过在作用域中放置一个未命名的(__compiler_generated_tmp在上面的代码中)变量,通常的生命周期规则将适用并执行您期望的操作。

于 2012-12-05T19:19:32.437 回答
1

std::string goo()中,一个 std::string 按值返回。

当编译器看到您在 main() 中调用此函数时,它会注意到返回值是 std::string,并在 main 堆栈上为 std::string 分配空间。

当 goo() 返回时, goo() 中的引用a不再有效,但 std::stringa引用被复制到 main() 中堆栈上保留的空间中

在这种情况下,可以进行多种优化,您可以在此处了解一个编译器可以做什么

于 2012-12-05T19:41:09.843 回答