【问题标题】:How is memory managed during C++ copy elision?在 C++ 复制省略期间如何管理内存?
【发布时间】:2020-08-24 00:14:48
【问题描述】:
#include <iostream>

using namespace std;

class A
{
public :
    A()
    {
        cout<<"constructor is called"<<endl;
    }
    
    ~A()
    {
        cout<<"destructor is called"<<endl;
    }
    
    A(const A &s)
    {
        cout<<"copy constructor is called"<<endl;
    }
};

A beta()
{
    A a;
    cout<<"mem location a : "<<&a<<endl;
    return a;
}

int main(int argc, char** argv) {
    A b = beta();
    cout<<"mem location b : "<<&b<<endl;
    return 0;
}

上述程序产生以下输出:

constructor is called
mem location a : 0x7ffc12bdaf77
mem location b : 0x7ffc12bdaf77
destructor is called

据我了解,由于复制省略或返回值优化,只创建了一个 A 实例,而不是 A aA b 的 2 个实例。 但是从内存的角度看上面的程序,object a 在函数 beta 的栈激活记录或栈空间内,内存位置为 0x7ffc12bdaf77。当它从 beta 返回时,复制省略使对象 b 与对象 a 相同,而不是复制它。所以,现在 b 也有地址 0x7ffc12bdaf77 。我无法理解 if b 仍然是函数 main 的本地并且存在于它的堆栈空间中,它怎么会占用 main 的堆栈空间之外的内存

【问题讨论】:

    标签: c++ c++11 copy-elision


    【解决方案1】:

    考虑这一行:A b = beta();。编译器是如何实现的?

    好吧,beta 是其他一些功能。而beta 的返回值是A。所以有两种可能的方法来实现这一点。编译器可以让beta 为其A 返回值分配堆栈空间,但这可能是有问题的,因为调用者需要使用该返回值。因此,编译器让 调用者 为返回值分配堆栈空间。毕竟,调用者确实知道返回值的大小/对齐方式,因此它拥有分配该空间所需的一切。

    所以让我们选择后者。这意味着当编译器调用beta 时,它会传入beta 的返回值所在的地址。但这也意味着编译器对于beta 的这个特定调用可以只给beta 的返回值提供与给b 相同的地址。

    因此,我们已经将函数返回值的副本删除到b

    所以当编译器去编译beta时,它知道调用者会给它一个指向返回值应该去哪里的指针。所以return a; 语句在语义上从a 变量复制到这个返回值对象中。

    但是,编译器可以看到整个beta。并且可以看到a 变量是一个局部变量,它会在所有控制路径上返回。因此,编译器可以将a 放入调用者为返回值提供的内存中,而不是给a 一个单独的堆栈地址。

    同样,我们已经从a 中删除了一个副本到返回值中。

    【讨论】:

      【解决方案2】:

      在 microsoft x64 调用约定中,隐藏指针将作为beta 的第一个参数添加。该指针包含b 的地址,它将位于main 的堆栈帧上。该指针将用于立即在main 的堆栈帧上同时创建ab。所以换句话说,a 存在于beta 的堆栈帧上;从内存的角度来看,ab 是等价的。

      【讨论】:

      • 这让我很吃惊。只能看到函数的声明的编译器如何知道这个隐藏指针?还是总是有一个指针传递给 optional 填充复制省略的返回值?
      • @AsteroidsWithWings:因为这是编译器编译返回值的函数的方式(或者更确切地说是一个太大或太复杂而无法放入寄存器的值)。它是调用约定的一部分,完全由函数的声明决定。
      • @NicolBolas 好的,所以这是 ABI 在所有情况下如何实现返回值的内置特性?说得通。我很困惑,因为在 C++17 之前,NRVO 是一种“优化”,因此您通常需要函数定义来知道它是否可以完成(即使那样,它“可能”还没有完成) .但是,经过进一步思考,我想一般来说没有其他方法可以实现这一点吗?正是因为这个原因。 :D
      • 然后我突然想到 C++17“保证省略”规则要求这种情况,因此每个 C++ ABI 都必须以这种方式工作,而不仅仅是 MS 的 x64 调用约定? (因为我不知道任何主流游戏形式上 C++17 的 ABI 损坏)
      • @AsteroidsWithWings:请记住,根据 C++,有 两个 副本:从返回到返回值对象的副本,以及从返回值对象到的副本接收端的堆栈变量。函数的实现可以省略一个,接收者可以省略另一个。如果两者都被省略,则完全省略。
      【解决方案3】:

      据我了解,只创建了一个 A 实例,而不是 2 个

      正确。

      它怎么会占用 main 堆栈空间之外的内存?

      没有。

      返回值只是在main调用的栈帧中创建。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2014-01-12
        相关资源
        最近更新 更多