使用“return”语句避免对象的副本

时间:2012-05-07 04:32:33

标签: c++ object copy return return-value

我在C ++中有一个非常基本的问题。 返回对象时如何避免复制?

以下是一个例子:

std::vector<unsigned int> test(const unsigned int n)
{
    std::vector<unsigned int> x;
    for (unsigned int i = 0; i < n; ++i) {
        x.push_back(i);
    }
    return x;
}

据我了解C ++是如何工作的,这个函数将创建2个向量:本地一个(x),以及将返回的x的副本。有没有办法避免副本? (我不想返回指向对象的指针,而是返回对象本身)


使用“移动语义”(在评论中说明)该函数的语法是什么?

7 个答案:

答案 0 :(得分:39)

对于RVO(返回值优化)的工作方式似乎存在一些困惑。

一个简单的例子:

#include <iostream>

struct A {
    int a;
    int b;
    int c;
    int d;
};

A create(int i) {
    A a = {i, i+1, i+2, i+3 };
    std::cout << &a << "\n";
    return a;
}

int main(int argc, char*[]) {
    A a = create(argc);
    std::cout << &a << "\n";
}

其输出位于ideone

0xbf928684
0xbf928684

令人惊讶?

实际上,这就是RVO的影响:要返回的对象 是在调用者中直接到位构建的。

如何?

传统上,调用者(此处main)将在堆栈上为返回值保留一些空间:返回插槽;被调用者(create here)以某种方式传递返回槽的地址以将其返回值复制到其中。然后,被调用者为其构建结果的局部变量分配自己的空间,就像任何其他局部变量一样,然后在return语句后将其复制到返回槽中。

当编译器从代码中推断出变量可以直接构造到具有等效语义的返回槽(as-if规则)时,就会触发RVO。

请注意,这是一种常见的优化,它由标准明确列入白名单,编译器不必担心副本(或移动)构造函数可能产生的副作用。

什么时候?

编译器最有可能使用简单的规则,例如:

// 1. works
A unnamed() { return {1, 2, 3, 4}; }

// 2. works
A unique_named() {
    A a = {1, 2, 3, 4};
    return a;
}

// 3. works
A mixed_unnamed_named(bool b) {
    if (b) { return {1, 2, 3, 4}; }

    A a = {1, 2, 3, 4};
    return a;
}

// 4. does not work
A mixed_named_unnamed(bool b) {
    A a = {1, 2, 3, 4};

    if (b) { return {4, 3, 2, 1}; }

    return a;
}

在后一种情况(4)中,在返回A时无法应用优化,因为编译器无法在返回槽中构建a,因为它可能需要其他东西(取决于布尔条件b)。

因此,一个简单的经验法则是:

如果在return语句之前没有声明返回槽的其他候选者,则应该应用

RVO。

答案 1 :(得分:17)

该程序可以利用命名的返回值优化(NRVO)。见这里:http://en.wikipedia.org/wiki/Copy_elision

在C ++ 11中,移动构造函数和赋值也很便宜。您可以在此处阅读教程:http://thbecker.net/articles/rvalue_references/section_01.html

答案 2 :(得分:14)

Named Return Value Optimization将为您完成工作,因为编译器在使用它时会尝试消除冗余的Copy构造函数和析构函数调用

std::vector<unsigned int> test(const unsigned int n){
    std::vector<unsigned int> x;
    return x;
}
...
std::vector<unsigned int> y;
y = test(10);

带有返回值优化:

  1. y已创建
  2. x已创建
  3. x被分配到y
  4. x被破坏
  5. (如果您想亲自尝试更深入了解,请查看this example of mine

    甚至更好,就像Matthieu M.指出的那样,如果在声明test的同一行内调用y,您还可以避免构造冗余对象和冗余分配好的(x将在将要存储y的内存中构建:

    std::vector<unsigned int> y = test(10);
    

    检查他的答案,以便更好地了解这种情况(你也会发现不能总是应用这种优化)。

    OR 你可以修改你的代码,将vector的引用传递给你的函数,这在语义上更正确,同时避免复制:

    void test(std::vector<unsigned int>& x){
        // use x.size() instead of n
        // do something with x...
    }
    ...
    std::vector<unsigned int> y;
    test(y);
    

答案 3 :(得分:2)

编译器通常可以为您优化掉额外的副本(这称为返回值优化)。见https://isocpp.org/wiki/faq/ctors#return-by-value-optimization

答案 4 :(得分:1)

引用它会起作用。

Void(vector<> &x) {

}

答案 5 :(得分:0)

如果未发生NRVO,则确保使用move构造函数

因此,如果按值返回带有move构造函数的对象(例如SELECT CustName, MIN(DECODE(DeptId,1,1,6,2,7,3,3,4,5,5,2,6,4,7)) as DeptId FROM tblCust GROUP BY CustName ORDER BY DECODE(DeptId,1,1,6,2,7,3,3,4,5,5,2,6,4,7) ),即使编译器无法进行可选的NRVO优化,也可以保证不进行完整的矢量复制。

有两个对C ++规范本身有影响的用户提到了这一点:

我对名人的呼吁不满意吗?

好。我不能完全理解C ++标准,但是可以理解其中的示例! ;-)

引用C++17 n4659 standard draft 15.8.3 [class.copy.elision]“复制/移动省略”

  

3在以下复制初始化上下文中,可能会使用移动操作代替复制操作:

     
      
  • (3.1)—如果return语句(9.6.3)中的表达式是一个(可能带括号的)id表达式,其名称为   在对象的主体或参数声明子句中声明了具有自动存储期限的对象   最里面的封闭函数或lambda表达式,或
  •   
  • (3.2)—如果throw-expression(8.17)的操作数是非易失性自动对象的名称(非   函数或子句参数),其范围不会超出最内层的末尾   包含try-block(如果有),
  •   
     

首先要执行重载分辨率以选择副本的构造函数,就像指定了对象一样   通过一个右值。如果第一个重载解析失败或没有执行,或者第一个参数的类型   所选构造函数的值不是对对象类型的右值引用(可能是cv限定),重载   再次执行解析,将对象视为左值。 [注意:此两阶段过载解决方案   无论是否会出现复制省略,都必须执行。它确定在以下情况下要调用的构造函数   不会执行省略,并且即使取消了调用,所选的构造函数也必须可访问。 - 结束   注意]

     

4 [示例:

std::vector
     

—结束示例

我不喜欢“可能被使用”这样的措辞,但我认为目的是意味着如果保持“ 3.1”或“ 3.2”,则必须发生右值返回。

这对我的代码注释非常清楚。

通过参考传递+ class Thing { public: Thing(); ~ Thing(); Thing(Thing&&); private: Thing(const Thing&); }; Thing f(bool b) { Thing t; if (b) throw t; // OK: Thing(Thing&&) used (or elided) to throw t return t; // OK: Thing(Thing&&) used (or elided) to return t } Thing t2 = f(false); // OK: no extra copy/move performed, t2 constructed by call to f struct Weird { Weird(); Weird(Weird&); }; Weird g() { Weird w; return w; // OK: first overload resolution fails, second overload resolution selects Weird(Weird&) } 进行多次通话

如果您要多次调用std::vector.resize(0),我相信这样做会更有效,因为当向量的大小加倍时,它可以节省几个test调用和重定位副本:

malloc()

假设https://en.cppreference.com/w/cpp/container/vector/resize说:

  

将向量容量调整为较小的大小时,永远不会减少其向量容量,因为这会使所有迭代器无效,而不是仅使由等效的pop_back()调用序列无效的那些迭代器无效。

而且我认为编译器无法优化按值返回的版本,以防止出现额外的malloc。

另一方面,这:

  • 使界面更丑陋
  • 减小向量大小时使用的内存超出了所需的内存

因此需要权衡。

答案 6 :(得分:-5)

首先,您可以将返回类型声明为std :: vector&amp;在这种情况下,将返回引用而不是副本。

您还可以定义一个指针,在方法体内构建一个指针,然后返回该指针(或该指针的副本是正确的)。

最后,许多C ++编译器可能会返回值优化(http://en.wikipedia.org/wiki/Return_value_optimization),在某些情况下会消除临时对象。