Morden C++ RVO NRVO std::move 之间的关系

返回值优化:

返回值优化 (RVO) 是一种 C++ 编译优化技术,当函数需要返回一个对象实例时候,就会创建一个临时对象并通过复制构造函数将目标对象复制到临时对象,这里有复制构造函数和析构函数会被多余的调用到,有代价,而通过返回值优化,C++ 标准允许省略调用这些复制构造函数。

Return Value Optimization (RVO), Named RVO (NRVO) and Copy-Elision are in C++ since C++98. 

 

那什么时候编译器会进行返回值优化呢?

  • return的值类型与函数的返回值类型相同,且 return 的是一个局部对象。

Return Value Optimization | Shahar Mike's Web Spot

To summarize, RVO is a compiler optimization technique, while std::move is just an rvalue cast, which also instructs the compiler that it's eligible to move the object. The price of moving is lower than copying but higher than RVO, so never apply std::move to local objects if they would otherwise be eligible for the RVO.

总而言之,RVO 是一种编译器优化技术,而 std::move 只是一种右值转换,它也指示编译器它有资格移动对象。移动的代价比复制低,但比 RVO 高,所以如果本地对象有资格获得 RVO,就不要对它们应用 std::move。

In g++/clang there exists flag -fno-elide-constructors which disables RVO. 对于 C++17 来说不能通过它来关闭 RVO 特性了,此编译选项和 -std=c++11 or -std=c++14 来使用。 

/Zc:nrvo (Control optional NRVO) | Microsoft Learn

The /Zc:nrvo compiler option controls Standard C++ optional named return value optimization (NRVO) copy or move elision behavior.  ---- MSVC

In Visual Studio 2022 version 17.4 and later, you can explicitly enable optional copy or move elision behavior by using the /Zc:nrvo compiler option. This option is off by default, but is set automatically when you compile using the /O2 option, the /permissive- option, or /std:c++20 or later. Under /Zc:nrvo, copy and move elision is performed wherever possible. Optional copy or move elision can also be explicitly disabled by using the /Zc:nrvo- option. These compiler options only control optional copy or move elision. Mandatory copy or move elision (specified by the C++ Standard) can't be disabled.

Improving Copy and Move Elision - C++ Team Blog (microsoft.com)

Copy/Move Elision 是什么?

当 C++ 函数中的 return 关键字后跟非内置类型的表达式时,执行该 return 语句会将表达式的结果复制到调用函数的返回槽(Return Slot)中。为此,将调用非内置类型的复制移动构造函数。然后,作为退出函数的一部分,将调用函数局部变量的析构函数,可能包括 return 关键字后面的表达式中命名的任何变量。

C++ 规范允许编译器直接在调用函数的返回槽中构造返回的对象,从而省略作为返回的一部分执行的复制或移动构造函数。与大多数其他优化不同,这种转换允许对程序的输出产生可观察的影响 – 即复制或移动构造函数以及关联的析构函数可以少调用一次

struct Snitch {   // Note: All methods have side effects
  Snitch() { cout << "c'tor" << endl; }
  ~Snitch() { cout << "d'tor" << endl; }

  Snitch(const Snitch&) { cout << "copy c'tor" << endl; }
  Snitch(Snitch&&) { cout << "move c'tor" << endl; }

  Snitch& operator=(const Snitch&) {
    cout << "copy assignment" << endl;
    return *this;
  }

  Snitch& operator=(Snitch&&) {
    cout << "move assignment" << endl;
    return *this;
  }
};

RVO (Return Value Optimization)

RVO 是一种编译器优化技术,它避免了从函数返回时创建临时对象。当函数返回一个临时对象(通常是由构造函数直接初始化的匿名对象)时,RVO 允许编译器省略创建和销毁临时对象的过程,而是直接在接收对象的位置(调用者提供的目标对象的内存空间)构造返回值

RVO basically means the compiler is allowed to avoid creating temporary objects for return values. This is the default behavior, meaning practically all C++ programs utilize RVO.

RVO 工作机制 ---- 当编译器确定可以进行 RVO 时,它会:

在调用者的栈帧上为返回值分配空间,而不是在被调用函数的栈帧上。

  1. 将返回值对象的地址传递给被调用的函数,这样被调用的函数就可以直接在该地址上构造对象。
  2. 允许函数直接在预分配的内存位置构造返回值,从而避免了额外的拷贝构造和析构调用。

The neat thing about RVO is that it makes returning objects free. It works via allocating memory for the to-be-returned object in the caller’s stack frame. The returning function then uses that memory as if it was in its own frame without the programmer knowing / caring.

Snitch ExampleRVO() {
  return Snitch();
}

int main() {
  Snitch snitch = ExampleRVO();
}

// g++ -std=c++11 -fno-elide-constructors main.cpp && ./a.out  //or -std=C++14

NRVO (Named Return Value Optimization)

NRVO 与 RVO 类似,但适用于返回函数内部已命名的局部变量。编译器优化这个过程,允许在调用者的栈帧上直接构造局部变量,避免了将局部变量拷贝到返回值的过程

Named RVO is when an object with a name is returned but is nevertheless not copied. 

NRVO 工作机制 ---- 在应用 NRVO 时,编译器会:

  1. 识别函数中将被返回的命名局部变量。
  2. 在调用者的栈帧上为该局部变量预留空间。
  3. 直接在该空间上构造局部变量,当函数返回时不需要移动或拷贝对象。

自 C++11 起才引入了 NRVO,而 NRVO 针对返回的对象是具名的,而 C++11 之前的 RVO 相对 NRVO 来说,RVO 优化针对的是返回一个未具名对象。While RVO is almost always going to happen, NRVO is more restricted.

Snitch ExampleNRVO() {
  Snitch snitch;
  return snitch;
}

int main() {
  ExampleNRVO();
}

// g++ -std=c++11 main.cpp && ./a.out

Copy Elision

RVO is part of a larger group of optimizations called copy-elision. Essentials are the same, except copy-elision is not required to happen as part of return statements, for example:

void foo(Snitch s) {
}

int main() {
  foo(Snitch());
}

这里的 copy-elision 就发生在函数参数传递时。

When RVO doesn’t / can’t happen

RVO is an optimization the compiler is allowed to apply (starting C++17 it is in fact required to in certain cases). However, even in C++17 it is not always guaranteed. Let’s look at a few examples.

1  函数中有多个 return 语句时,RVO 不会发生。

When the compiler can’t know from within the function which instance will be returned it must disable RVO:

nitch CreateSnitch(bool runtime_condition) {
  Snitch a, b;
  if (runtime_condition) {
    return a;
  } else {
    return b;
  }
}

int main() {
  Snitch snitch = CreateSnitch(true);
}

2 Returning a Parameter / Global   函数返回参数变量,或者全局变量时

When returning an object that is not created in the scope of the function there is no way to do RVO:

Snitch global_snitch;

Snitch ReturnParameter(Snitch snitch) {
  return snitch;
}

Snitch ReturnGlobal() {
  return global_snitch;
}

int main() {
  Snitch snitch = ReturnParameter(global_snitch);
  Snitch snitch2 = ReturnGlobal();
}

// g++ -std=c++17 -stdlib=libc++ main.cpp && ./a.out  // or -std=c++20

3 Returning by std::move() 使用 std::move 在返回值时会阻止编译器进行 RVO NRVO

Returning by calling std::move() on the return value is an anti-pattern. It is wrong most of the times. It will indeed attempt to force move-constructor, but in doing so it will disable RVO. It is also redundant, as move will happen if it can even without explicitly calling std::move() (see here).

Snitch CreateSnitch() {
  Snitch snitch;
  return std::move(snitch);
}

int main() {
  Snitch snitch = CreateSnitch();
}

4 Assignment  非拷贝赋值和移动赋值外的赋值 = 操作。

RVO can only happen when an object is created from a returned value. Using operator= on an existing object rather than copy/move constructor might be mistakenly thought of as RVO, but it isn’t:

Snitch CreateSnitch() {
  return Snitch();
}

int main() {
  Snitch s = CreateSnitch();
  s = CreateSnitch();
}

5 Returning Member   返回数据成员时,即使是未具名数据成员。

In some cases even an unnamed variable can’t RVO:

struct Wrapper {
  Snitch snitch;
};

Snitch foo() {
  return Wrapper().snitch;
}

int main() {
  Snitch s = foo();
}

结论:

While we can’t count on RVO to always take place, it will in most cases. For those cases where it doesn’t we always have Move Semantics, which is the topic of the next post. As always, optimize for readability rather than performance when writing code, unless you have a quantifiable reason.

虽然我们不能指望RVO总是发生,但它在大多数情况下都会发生对于那些没有的情况,我们总是有移动语义(来帮助我们避免 copy)。一如既往,在编写代码时,优化可读性而不是性能,除非你有一个可以量化的原因。

std::move  RVONRVO

std::move 是 C++11 中引入的一个标准库函数,它可以将对象的状态或所有权从一个实例转移到另一个实例,而不需要拷贝内容。它将左值强制转换为右值引用,使得可以使用移动语义而不是拷贝语义

使用 std::move 在返回值时会阻止编译器进行 RVO NRVO。这是因为 std::move 强制将对象视为右值,即使它是一个局部变量。编译器必须假设这个对象的资源可能已经被外部引用,因此不能在原地构造返回值。

  • 返回局部对象时,应该避免使用 std::move,以便编译器可以尽可能地应用 RVO 或 NRVO。只需简单地返回对象即可。
  • 当你有一个将不再使用的对象,并且想要转移其资源时,使用 std::move 是合适的。例如,在将对象作为右值传递给构造函数或函数时:在这种情况下,std::move 是正确的选择,因为它允许对象 obj 的资源被转移到函数 process 中,而不是进行拷贝。
MyClass createMyClass() {

    MyClass obj;

    return obj; // RVO 或 NRVO 可能会应用

}
void process(MyClass&& obj);

MyClass obj;

process(std::move(obj)); // obj 的状态被转移

【Modern C++】深入理解左值、右值 (qq.com)

编译器之返回值优化 (qq.com)

Move Semantics 与 RVO 的关系

Move Semantics | Shahar Mike's Web Spot

Move Semantics are a C++11 feature which complements C++98’s RVO; Think of them as user-defined RVO-like optimization. While originally designed to only allow optimizations, one can also utilize move semantics to limit APIs. This is how std::unique_ptr is able to be a move-only type, allowing it to enforce single ownership (more about std::unique_ptr here).

移动语义是 C++11 的一个特性,它补充了 C++98 的RVO可以把它们看作是用户定义的类似 RVO 的优化。虽然最初的设计只允许优化,但是也可以利用移动语义来限制API。这就是std::unique_ptr 能够成为只移动类型的原因,允许它实施单一所有权(这里有更多关于std::unique_ptr 的信息)。

As we saw previously, RVO does not always take place. When it doesn’t, C++98 forced users to create expensive copies.

Move 语义和 Move Constructor / Move Assignment

RVO is not allowed on assignment.(见上面分析)

class MyClass {
 public:
  MyClass();                             // Constructor

  MyClass(const MyClass& o);             // Copy constructor
  MyClass(MyClass&& o);                  // Move constructor

  MyClass& operator=(const MyClass& o);  // Copy assignment
  MyClass& operator=(MyClass&& o);       // Move assignment
};

Move constructors / assignment operations will be invoked automatically by the compiler only if the parameter passed to them (o in the above example) are rvalues. Otherwise the compiler will invoke the safe-but-slow copy constructor / assignment.  --- 右值最基本的意思是临时的不可取地址的。you know that o will very soon need to be destroyed.

std::string& operator=(std::string&& o) {  // `o` is a temporary
  // Steal & copy data
  data_ = o.data_;  // data_ is a char*
  size_ = o.size_;  // size_ is a size_t
  capacity_ = o.capacity_;  // capacity_ is a size_t

  // Make sure `o` can be destroyed safely
  o.data_ = nullptr;
  // We can also do o.size_ = o.capacity_ = 0;

  return *this;
}

move assignment operator :No memory allocation, no copying of buffer, O(1) operation. That’s much better than copy assignment!

小结 Interim Summary

This special syntax, std::string&& o, is our entry point to using move semantics. Furthermore, this assignment operation is not a copy assignment but rather a move assignment. And && means that we have a special reference in our hands - a reference to a “temporary object.

std::move()

Any function (such as move constructor, move assignment, or just a global function) which accepts rvalue references (&&) can only be called with an rvalue object. This is where the true power of move semantics comes into play. The compiler knows when it’s safe to pass an object as an rvalue reference. If it’s not, we’ll get a compile error:

void foo(std::string&& s) { /* ... */ }

// ...

foo("hello");  // Temporary objects are always rvalues.

// Return values are rvalues as well (except when the function returns a
// reference)
foo(BuildLongString());

std::string s;
foo(s);  // Compile error - `s` isn't an rvalue

This is good, and by design. However there are cases where we do want to convert an object to an rvalue - where we know it won’t be used in the future. What then? This is where std::move() comes into play:

// std::move() converts an object to rvalue.
foo(std::move(s));

Rule of 3 becomes Rule of 5 C++三五原则

One last thing before I wrap up: If you ever heard of the rule of three - with move semantics we now have a complementary rule - the rule of 5.

C++三五原则(The rule of three/five/zero)讲述了相应的 C++ 特殊成员函数之间的依赖关系,以及 C++类中资源管理(拷贝控制)的一些基本法则。

 三/五/零之法则 - cppreference.com   or   The rule of three/five/zero - cppreference.com                 The Rule Of 0, 3, 5 and 4.5 - 知乎 (zhihu.com)

CppCon笔记--Back to Basics: RAII and the Rule of Zero - linsinan1995 - 博客园 (cnblogs.com)

参考:CppCon2019:Back to basics: RAII and The Rule of Zero

Modern C++ 特殊成员函数之间的依赖 - 知乎 (zhihu.com)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值