从技术上讲,如何在不复制的情况下移动函数返回值

C++ how is it technically possible to move a function return value without a copy?

本文关键字:情况下 移动 函数 返回值 复制 从技术上      更新时间:2023-10-16

当一个函数返回一个值时,这个值被放在堆栈上(函数堆栈帧被删除,但返回值保留在那里,直到调用者得到它)。

如果返回值在堆栈上,移动如何获得该值而不将其复制到可变内存位置?

例如:

A a = getA();

在许多c++实现中,返回"复杂"数据类型的函数被传递一个隐藏参数,该参数是指向返回实例所在空间的指针。本质上,编译器将Foo r = fun();转换为

char alignas(Foo) r[sizeof Foo]; // Foo-sized buffer, unitialized!
fun(&r);

可以看到,Foo是在调用方帧的堆栈上分配的。现在,在fun的实施中可能会有一个副本。建设

Foo fun() {
  Foo rv;
  ...
  return rv;
}

通常实现为

void fun(Foo * $ret) {
  Foo rv;
  ..
  new ($ret) Foo(rv); // copy construction
}

当应用返回值优化时,它被更改为

void fun(Foo * $ret) {
  Foo & rv = *(new ($ret) Foo);
  ...
  return;
}

现在没有涉及到复制。

用于存储用于从函数返回值的临时值的存储(堆,堆栈,寄存器等)是实现定义的。你可以看到它是:

+-----------------------------+-------------------------+-----------------------------------------+
| target (caller stack frame) | temporary (unspecified) | return statement (function stack frame) | 
+-----------------------------+-------------------------+-----------------------------------------+ 

值从右向左传递。还要注意,标准规定任何编译器都可以省略临时和赋值/复制/移动,并直接初始化目标。

写一个类,如:

class trace
{
public:
    trace()
    { 
        std::cout << "Init" << std::endl;
    }
    ~trace()
    { 
        std::cout << "Destroy" << std::endl;
    }
    trace( const trace& )
    { 
        std::cout << "Copy init" << std::endl;
    }
    trace( trace&& )
    { 
        std::cout << "Move init" << std::endl;
   }
    trace& operator=( const trace& )
    { 
        std::cout << "Copy assign" << std::endl;
    }
    trace& operator=( trace&& )
    { 
        std::cout << "Move assign" << std::endl;
    }
};

和尝试不同的编译器优化是很有说明意义的。

假设A是聚合或原语。如果这是真的,那么yes move和copy语义是等价的。然而,如果A是像vector这样的复杂类型,那么它将包含指向资源的指针。当移动对象时,指针被复制而不复制它们所指向的值。

当你说"move"时,我假设你指的是c++ 11的move构造函数和std::move函数。

移动一个对象实际上并没有移动整个对象。它使用它的move构造函数构造一个新的对象,该构造函数被允许获取原始对象持有的资源的所有权,而不是复制它们。例如,如果你写:

std::vector<int> foo = function_that_returns_a_vector();

编译器可以通过调用foo的move构造函数并将函数返回的临时向量传递给它来实现这一点。move构造函数将获得临时向量指向其堆分配内容的内部指针的所有权,使临时向量为空。在c++ 11和move支持之前,foocopy构造函数会被调用,它会在堆上分配新的空间来复制返回向量的内容,即使该返回向量即将被销毁并且不再需要自己的副本。

请注意,编译器根本不必通过从返回的临时对象构造foo来实现这一行。根据编译器特定于平台的调用约定,可以将(未初始化的)foo变量的地址以这样一种方式传递给函数,即函数的返回值直接构造为foo,从而避免在函数返回后进行复制。这就是所谓的复制省略。

最简单的方法是这样修改你的方法:

void getA(A& out);
A a;
getA(a);

但是,编译器会尽力避免这种多余的内容: