Skip to content

返回值优化

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#include <iostream>
class Obj {
 public:
  Obj() {  // 构造函数
    std::cout << "in Obj() "
              << " " << this << std::endl;
  }
  Obj(int n) {
    std::cout << "in Obj(int) "
              << " " << this << std::endl;
  }
  Obj(const Obj &obj) {  // 拷贝构造函数
    std::cout << "in Obj(const Obj &obj) " << &obj << " " << this << std::endl;
  }
  Obj &operator=(const Obj &obj) {  // 赋值构造函数
    std::cout << "in operator=(const Obj &obj)" << std::endl;
    return *this;
  }
  ~Obj() {  // 析构函数
    std::cout << "in ~Obj() " << this << std::endl;
  }
  int n;
};
Obj fun() {
  // Obj obj;
  // do sth;
  // return obj;//具名
  return Obj();//非具名
}
int main() {
  Obj obj = fun();
  std::cout << "&obj is " << &obj << std::endl;
  return 0;
}

没有优化

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
Obj fun() {
   Obj obj;//具名
   return obj;

}
int main() {
  Obj obj = fun();
  std::cout << "&obj is " << &obj << std::endl;
  return 0;
}

调用的Obj类成员函数的顺序应该为:

  1. 调用构造函数,生成对象
  2. 调用拷贝构造函数,生成临时对象
  3. 析构第1步生成的对象
  4. 调用拷贝构造函数,将第2步生成的临时变量拷贝到main()函数中的局部对象obj中
  5. 调用析构函数,释放第2部生成的临时对象
  6. 调用析构函数,释放main()函数中的obj局部对象

1
2
3
4
5
6
7
8
9
g++ -std=c++11 -fno-elide-constructors -g test.cc -o test && ./test

in Obj()  0x7ffee18a9a00 // 在fun()函数中,构造obj对象
in Obj(const Obj &obj) 0x7ffee18a9a00  0x7ffee18a9a40 // 通过拷贝构造创建临时变量(fun()函数定义的obj--->临时对象)
in ~Obj() 0x7ffee18a9a00 // 析构fun()函数中构造的obj对象(fun()函数定义的obj)
in Obj(const Obj &obj) 0x7ffee18a9a40 0x7ffee18a9a30 // 通过拷贝构造函数构建obj(main函数中的)对象(临时对象--->main()函数定义的obj)
in ~Obj() 0x7ffee18a9a40 // 释放临时对象
&obj is 0x7ffee18a9a30 
in ~Obj() 0x7ffee18a9a30 // 释放main()函数中定义的obj对象
一共调用了一次构造,两次拷贝构造,三次析构

有优化

1
2
3
4
5
g++ -g -std=c++11 test.cc -o test && ./test

in Obj()  0x7ffd6fb15240
&obj is 0x7ffd6fb15240
in ~Obj() 0x7ffd6fb15240
一共调用了一次构造一次析构

原理

RVO(Return Value Optimization)返回值优化,或者NRVO,又名具名返回值优化(Named Return Value Optimization),会把原来fun里边的构造行为放到main中进行构造。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
void fun(Obj &_obj) {
  Obj obj(1);
  _obj.Obj::Obj(obj); // 拷贝构造函数
  return;
}

int main() {
  Obj obj; // 仅定义不构造
  fun(obj);
  return 0;
}

不能优化的几种情况

根据不同条件返回不同变量

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
Obj fun(bool flag) {
  Obj o1;
  Obj o2;
  if (flag) {
    return o1;
  }
  return o2;
}

int main() {
  Obj obj = fun(true);
  return 0;
}

返回全局变量

当返回的对象不是在函数内创建的时候,是无法执行返回值优化的。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
Obj g_obj;

Obj fun() {
  return g_obj;
}

int main() {
  Obj obj = fun();
  std::cout << &obj << std::endl;
  return 0;
}

返回函数参数

与返回全局变量类似,当返回的对象不是在函数内创建的时候,是无法执行返回值优化的。

代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
Obj fun(Obj obj) {
  return obj;
}

int main() {
  Obj o;
  Obj obj = fun(o);
  std::cout << "in main " << &obj << std::endl;
  return 0;
}

编译并运行之后,输出:

1
2
3
4
5
6
7
in Obj() 0x7ffdbb43da00
in Obj(const Obj &obj) 0x7ffdbb43da00 0x7ffdbb43da10
in Obj(const Obj &obj) 0x7ffdbb43da10 0x7ffdbb43d9f0
in ~Obj() 0x7ffdbb43da10
in main 0x7ffdbb43d9f0
in ~Obj() 0x7ffdbb43d9f0
in ~Obj() 0x7ffdbb43da00

返回成员变量

在某些特殊情况下,即使是未具名变量,也不能RVO。

代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
struct Wraper {
   Obj obj;
 };

Obj fun() {
  return Wraper().obj;
}

int main() {
  Obj obj = fun();
  std::cout << &obj << std::endl;
  return 0;
}

编译并运行,结果如下:

1
2
3
4
5
in Obj()  0x7ffed7f85290 // 构造Wraper中的obj对象
in Obj(const Obj &obj) 0x7ffed7f85290 0x7ffed7f852c0 // 通过拷贝赋值给main函数中的局部变量
in ~Obj() 0x7ffed7f85290 // 析构Wraper中的obj对象
0x7ffed7f852c0
in ~Obj() 0x7ffed7f852c0 // 析构main中的局部对象

使用move

在返回值上调用std::move()进行返回是一种错误的方式。它会尝试强制调用移动构造函数,但这样会导致RVO失效。因为即使没有显示调用std::move(),编译器优化中也会执行move操作。

代码如下:

1
2
3
4
5
6
7
8
9
Obj fun() {
  Obj obj;
  return std::move(obj);
}

int main() {
  Obj obj = fun();
  return 0;
}

输出如下:

1
2
3
4
5
in Obj()  0x7ffe7d4d1720
in Obj(const Obj &&obj)//增加了一次
in ~Obj() 0x7ffe7d4d1720
0x7ffe7d4d1750
in ~Obj() 0x7ffe7d4d1750

从上面输出可以看出,与不使用std::move()返回相比,使用std::move()返回增加了一次拷贝构造调用和一次析构调用。

return move(x)

大多数时候没有意义,但有的时候会有意义。

没意义的

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
#include<iostream>

struct X{
    X() { puts("X()"); }
    X(const X&) { puts("X(const X&)"); }
    X(X&&)noexcept { puts("X(X&&)"); }
    ~X() { puts("~X()"); }
};

X f(){
    X x;
    return std::move(x);
}

int main(){
    X x = f();
}

运行结果

1
2
3
4
X()
X(X&&)
~X()
~X()

可以说毫无意义,甚至影响NRVO优化(在当前语境,即使不std::movereturn x,重载决议一样会选择到移动构造。这里的x是隐式可移动实体)。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
#include<iostream>

struct X{
    X() { puts("X()"); }
    X(const X&) { puts("X(const X&)"); }
    X(X&&)noexcept { puts("X(X&&)"); }
    ~X() { puts("~X()"); }
};

X f(){
    X x;
    return x;
}

int main(){
    X x = f();
}

运行结果

1
2
X()
~X()

以上去除了std::move,编译器得以正常NRVO优化,优化了移动构造的开销 。

如果禁用了NRVO,那么运行结果其实和上面的是一样的。 gcc 加上 -fno-elide-constructors选项即可。

如果以上用法只能说毫无意义,可能影响性能,但是并不会导致什么错误和问题。那么下面我说的就是真正具备各种危害的了。

1
2
3
4
X&& f(){
    X x;
    return std::move(x);
}
引用了局部的对象,函数作用域结束,返回的引用是悬垂的,已然ub了。

有意义的

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include<iostream>

struct X{
    X() { puts("X()"); }
    X(const X&) { puts("X(const X&)"); }
    X(X&&)noexcept { puts("X(X&&)"); }
    ~X() { puts("~X()"); }
};

struct Test{
    X x;//数据成员 可以return move
    X f(){
        return std::move(x);//数据成员不是隐式可移动实体,如果不std::move,直接return x,重载决议不会选择移动构造。
    }
    X f2() {
        return x;
    }
};

int main(){
    Test t;
    puts("---------");
    t.f();
    puts("---------");
    t.f2();
    puts("---------");
}

运行结果

1
2
3
4
5
6
7
8
9
X()
---------
X(X&&)
~X()
---------
X(const X&)
~X()
---------
~X()

数据成员不是隐式可移动实体,如果不 std::move,而是直接 return x,重载决议不会选择移动构造

或者至少需要稍微提一下

1
2
3
4
X foo() {
  X x
  return x
}

1
2
3
4
5
6
class Y {
  X x
  X foo() {
    return x
  }
}

是完全不一样的。