跳过正文
  1. Posts/

[施工中][c++11] move semantics && perfect forwarding 学习笔记

·1461 字·3 分钟
77 - 这篇文章属于一个选集。

背景
#

move semantics 是 modern cpp 中非常重要的特性,有必要详细了解一下。

update time:2022 年 7 月 2 日

move semantic
#

基本的内容大家都很熟悉,就不说了

std::move 做了什么
#

std::move 没有 move 任何内容,只是简单地把传进来的参数转换为对应的 rvalue reference。

实现为:

1template<typename T>
2constexpr std::remove_reference_t<T>&& move(T&& t) noexcept
3{
4    return static_cast<std::remove_reference_t<T>&&>(t);
5}
需要强调的是,无论传入的是const or non-const, lvalue or rvalue, ref or non-ref, std::move 都会无条件(无差别)地转换为 rvalue reference

被std::move的值处于一个什么状态
#

moved from object is in a valid but unspecified state

说人话就是,对于 moved from object,仍然可以对其进行一些操作,但是其 state 是未知的。

move 的本质是资源所有权的转移:

move 语义:std::move 只是把源对象转换成 rvalue reference,真正的资源转移发生在 move 构造或 move 赋值,源对象随后进入 valid but unspecified 状态

我们拆开来看

这里提到的一些操作,其实只有三种:

  • 析构函数
  • move assignment operator
  • copy assignment operator

其余操作都是非法的。

为什么 state 是未知的呢?因为这和实现有关。 标准只要求 moved from object 是可以被析构的,但是内部是什么值,其实是不确定的。 参考 cpp core guidelines C.64

Ideally, that moved-from should be the default value of the type. Ensure that unless there is an exceptionally good reason not to. However, not all types have a default value and for some types establishing the default value can be expensive. The standard requires only that the moved-from object can be destroyed. Often, we can easily and cheaply do better: The standard library assumes that it is possible to assign to a moved-from object. Always leave the moved-from object in some (necessarily specified) valid state.

此外,由于moved from object在出了所在的scope后就会调用析构函数,因此称之为一个 “expiring value(xvalue)",也就是临近生命周期终点的"将亡值”。详细可以参考 浅谈 Cpp Value Categories

when not to use std::move
#

最常见的一种情景是,在函数 return 时错误地使用了 std::move,从而阻止了编译器可能的 copy elision。

1T fn()
2{
3  T t;
4  return std::move(t);
5}

实际上,编译器会先尝试 copy elision,如果做不到,那么会隐式地做 std::move。

因此,无论如何都不要把 std::move 用在函数返回值上。

forwarding reference
#

其实就是 universal reference,forwarding reference 是官方名称。

1template<typename T>
2void f(T&& x);   // forwarding reference
3
4auto && var2 = var1;  // forwarding reference

forwarding reference 的规则:

  • 被 lvalue 初始化就是 lvalue reference
  • 被 rvalue 初始化就是 rvalue reference

forwarding reference 从语法上看起来和rvalue reference 还是比较相像的,那如何区分呢? 如下情况时,实际上是forwarding reference而不是rvalue reference

  • 发生了 type deduction
  • 从形式上看,出现了 T&& 或者 auto&&

perfect forwarding
#

完美转发,也叫精确传递,指的是“需要将一组参数原封不动的传递给另一个函数”。

原封不动的含义是说:数值不变,左值/右值属性不变,const/non-const属性不变。

在泛型函数中,这样的需求非常普遍。

比如下面这个例子:

 1template <typename T> void forward_value(const T& val) {
 2    process_value(val);
 3}
 4template <typename T> void forward_value(T& val) {
 5    process_value(val);
 6}
 7
 8int a = 0;
 9const int &b = 1;
10forward_value(a); // int&
11forward_value(b); // const int&
12forward_value(2); // int&

在右值引用出现之前,模板的每个参数都必须重载两个类型,总的重载次数是指数级的 orz,警察听了想打人

然而有了右值引用,不管模板有多少个参数,我们只需要定义一次,接受一个右值引用的参数,就能够将所有的参数类型原封不动地传递给目标函数。

1template <typename T> void forward_value(T&& val) {
2    process_value(val);
3}
4
5int a = 0;
6const int &b = 1;
7forward_value(a); // int&
8forward_value(b); // const int&
9forward_value(2); // int&&

那么最后说回 std::move。std::move 的作用就是传入左值,返回右值引用,从而利用右值引用带来的好处(节省资源、实现完美转发等)。

 1#include <iostream>
 2#include <utility>
 3#include <vector>
 4#include <string>
 5
 6int main()
 7{
 8    std::string str = "Hello";
 9    std::vector<std::string> v;
10
11    // 使用 push_back(const T&) 重载,
12    // 表示我们将带来复制 str 的成本
13    v.push_back(str);
14    std::cout << "After copy, str is \"" << str << "\"\n";
15
16    // 使用右值引用 push_back(T&&) 重载,
17    // 表示不复制字符串;而是
18    // str 的内容被移动进 vector
19    // 这个开销比较低,但也意味着 str 现在可能为空。
20    v.push_back(std::move(str));
21    std::cout << "After move, str is \"" << str << "\"\n";
22
23    std::cout << "The contents of the vector are \"" << v[0]
24                                         << "\", \"" << v[1] << "\"\n";
25}
26After copy, str is "Hello"
27After move, str is ""
28The contents of the vector are "Hello", "Hello"

参考资料:

77 - 这篇文章属于一个选集。

相关文章

[C++11] promise && future leanrning notes

·322 字·1 分钟
用人话就是,主线程传给附属线程一个promise Object,然后主线程想要获取附属线程set给promise Object的值(也就是该线程返回的某个结果),需要通过主线程中的promise object 得到对应的future object(每个promise 对应一个 future),然后调用future 的get方法。如果附属线程没有执行作为参数传入的promise的set方法去返回结果,那么程序就会block住。

把二进制文件按字节读到vector中

·197 字·1 分钟
1 std::vector<unsigned char> readFromFile1(const char* filePath) { 2 FILE* file = fopen(filePath, "rb"); 3 std::vector<unsigned char> result; 4 if (file == nullptr) { 5 return result; 6 } 7 8 // 获取文件大小,尽量一次读完 9 size_t fileSize = getFileSize(file); 10 if (fileSize != 0) { 11 result.resize(fileSize); 12 size_t n = fread(&result[0], 1, fileSize, file); 13 assert(n <= fileSize); 14 if (n != fileSize) { 15 result.resize(n); 16 } 17 } 18 19 // 在读取过程当中,有可能文件大小有变化,再尝试读取 20 const size_t read_len = 1024; 21 char buf[read_len]; 22 for (;;) { 23 size_t n = fread(buf, 1, read_len, file); 24 result.insert(result.end(), buf, buf + n); 25 if (n < read_len) { 26 break; 27 } 28 } 29 fclose(file); 30 return result; 31 } 另外一种C++风格,但是性能较差的方法: