C++右值引用和完美转发

C++右值引用和完美转发

    • 何为引用
      • 引用必须是左值
    • 右值引用
      • 完美转发
    • move()
      • 使用move的优点
      • move 左值测试
      • move 右值测试
      • 注意
    • 参考链接


看到有些同学,调用函数的时候总喜欢使用std::move希望避免一些开销,而实际上由于他并不理解什么是右值引用、完美转发,导致这种努力成为了徒劳,反增笑柄。

本文为记录我学习右值引用和完美转发的笔记。

何为引用

C++新增了一种复合类型,也就是引用变量。通过引用,就可以使用该引用名称或变量名称来指向变量。

引用必须是左值

对于对象的引用必须是左值(常量引用除外)
const引用能够绑定到临时对象, 并将临时对象的生命周期由”创建临时对象的完整表达式”提升至”绑定到的const引用超出作用域”。 non-const 引用没有这个功能

const int& a = 101;//对
int& b = 101;//错

右值引用

右值引用可以从字面意思上理解,指的是以引用传递(而非值传递)的方式使用 C++ 右值。用 “&&” 表示。

完美转发

  • 使用forward()再模板可以做到完美转发,减少拷贝
  • 完美转发的好处是函数可以动态的接受函数参数,从而免去了有可能的拷贝
  • 完美转发的函数内部,需要使用forward()来把接受的参数转化为合适的形式传递出去

我们看Chromium提供的例子:

#include 
#include 

class MyType {
 public:
  MyType() {}
  MyType(MyType&& other) { fprintf(stderr, "move ctor\n"); }
  MyType(const MyType& other) { fprintf(stderr, "copy ctor\n"); };
};

void Store(const MyType& type) {
  fprintf(stderr, "store (copy)\n");
}

void Store(MyType&& type) {
  fprintf(stderr, "store (move)\n");
}

template<typename T>
void ProcessAndStore(T&& var) {
  // Process
  // ...

  // The type of |var| could be an rvalue reference, which means we should pass
  // an rvalue to Store. However, it could also be an lvalue reference, which
  // means we should pass an lvalue.
  // Note that just doing Store(var); will always pass an lvalue and doing
  // Store(std::move(var)) will always pass an rvalue. Forward does the right
  // thing by casting to rvalue only if var is an rvalue reference.
  Store(std::forward<T>(var));
}

int main(int argc, char **argv) {
  MyType type;
  // In ProcessAndStore: T = MyType&, var = MyType&
  ProcessAndStore(type);
  // In ProcessAndStore: T = MyType, var = MyType&&
  ProcessAndStore(MyType());
}

move()

  • 这个函数从C++11开始也变为STL函数了

  • 移动赋值函数

  • std::move函数可以以非常简单的方式将左值引用转换为右值引用

  • 应用之一是unique_ptr

  • 移动之后括号内的值就不要再用了, move之后本身就会析构, functions that receive rvalues may act destructively on your variable, so using the variable’s contents afterward may result in undefined behaviour. The only valid things you may do after calling std::move() on a variable are:

    • Destroy it
    • Assign to it (ie. replace its contents)
    • 上一点的原因是,rvalue reference的语义是函数可以认为rvalue外面没有再被引用了。所以可以浅拷贝,可以析构,如果你还要用里面的值的话,就会有问题。
      • doing std::move() on an lvalue reference is bad!!! Code producing an lvalue reference expects the object to remain valid. But code receiving an rvalue reference expects to be able to steal from it. This leaves you with a reference pointing to a potentially-invalid object.
    • 一个例外是当函数参数类型是到模板形参的右值引用(“转发引用”或“通用引用”)时,该情况下转而使用 std::forward
#include 
std::vector<std::string> v;
std::string str = "example";
v.push_back(std::move(str)); // str is now valid but unspecified
str.back(); // undefined behavior if size() == 0: back() has a precondition !empty()
str.clear(); // OK, clear() has no preconditions

使用move的优点

move用于移动构造时,可以使移动构造函数成为浅拷贝,由于rvalue出去也没有引用了,所以很安全,性能也比深拷贝要好

#include 
#include 
#include 

class MyType {
 public:
  MyType() {
    pointer_ = new int;
    *pointer_ = 1;
    memset(array_, 0, sizeof(array_));
    vector_.push_back(3.14);
  }

  MyType(MyType&& other) {
    fprintf(stderr, "move ctor\n");

    // Steal the memory, null out |other|.
    // 因为other会被析构,所以要把它赋值为null,之所以敢这么做,因为传进来的是个右值,外面没有人用了。
    pointer_ = other.pointer_;
    other.pointer_ = nullptr;

    // Copy the contents of the array.
    memcpy(array_, other.array_, sizeof(array_));

    // Swap with our (empty) vector.
    vector_.swap(other.vector_);
  }

  ~MyType() {
    delete pointer_;
  }

 private:
  int* pointer_;
  char array_[42];
  std::vector<float> vector_;
};

void ProcessMyType(MyType type) {
}

MyType MakeMyType(int a) {
  if (a % 2) {
    MyType type;
    return type;
  }
  MyType type;
  return type;
}

int main(int argc, char **argv) {
  // MakeMyType returns an rvalue MyType.
  // Both lines below call our move constructor.
  MyType type = MakeMyType(2);
  ProcessMyType(MakeMyType(2));
}

move 左值测试

struct testc {
    int a;
}
void test_func(testc a) {};
void test_func_1(testc &&a) {};
int main() {
    testc lv;
    testc &a = lv;  //左值引用,没有构造
    testc &&b = static_cast<testc&&>(lv);  // 右值引用,没有构造

    testc c = std::move(lv);  // 把一个右值引用赋值给一个左值,如果有移动构造函数,则调用移动构造函数。没有则调用拷贝构造函数
    test_func(std::move(lv)); // 这个move仅仅是把调用test_func时候创建形参的过程从拷贝构造变成了移动构造。并没有减多少开销
    test_func_1(std::move(lv));  // 没有构造函数,test_func_1直接对形参进行右值引用
    testc c1;
    c1 = std::move(lv);  // 这两句话更惨,首先构造了c1,调用了构造函数,然后调用了operator=(testc &c)赋值
    c1 = lv;  // 同上,是否使用move并没有带来额外的开销。
    testc &d = std::move(lv);
    testc &&e std::move(lv);  // 右值引用幅值给右值引用,没有构造, lv仍然可以对结构体进行操作
    testc f = lv;   // 拷贝构造函数,注意这里没有调用operator+
}

move 右值测试

struct testc {
    int a;
    testc (testc &&other) {
       	a = c.a;
    }
    // 移动构造还有一种写法
    testc(testc&& other) {

        // Note that although the type of |other| is an rvalue reference,

        // |other| itself is an lvalue, since it is a named object. In order

        // to ensure that the move assignment is used, we have to explicitly

        // specify std::move(other).
        *this = std::move(other);
    }
}
testc rv() {
return testc();
}
testc rv(int a) {
  // This is here to circumvent some compiler optimizations,
  // to ensure that we will actually call a move constructor.
  // 这点比较精髓,如果没有这个wrapper函数,可能所有的左值右值的构造都会被编译器优化掉
    if (a % 2) {
        testc c;
        return c;
    }
    testc c;
    return c;
}

void receiver(testc &&a) {
    ...
}

int main() {
  testc lv;  // 构造函数
  lv = rv(2);  // rv里面会调用构造函数,构造一个临时右值,然后移动构造到一个临时变量(第一个临时变量析构),然后这个临时变量再调用operator=复制,然后rv的临时右值析构(第二个临时变量析构)
  testc a = rv();  // rv里面调用构造函数,然后a就会直接用这个对象,这中间没有任何构造析构拷贝了,原因是因为编译器优化掉了。
  testc a1 = rv(2); // rv里面调用构造函数,然后移动构造!!!(如果没有定义移动构造函数,那么就用拷贝构造函数)。然后rv(2)出来的临时右值被析构
  testc &&b = rv(2);  // 同上。rv里面调用构造函数,然后移动构造到一个新的地方,rc里面的析构,b指向的是新拷贝的一块地方  (由此可见,使用一个右值引用之后,实际上是另外开辟了一个空间存储,这时这个变量就变成左值了!)
  testc &&c = static_cast<testc&&>(rv(1));  // 同上  
  testc &&d = static_cast<testc>(rv(1));  // 同上  
  testc &&d1 = std::move(lv);  // 同上,mv创建一个区域,然后lv移动构造过去,持有的是那片空间
  testc &&e = std::move(rv(2));  // 同上,但是感觉move那个地方还会有一层析构,并且程序结束之后就不会析构了,不要这么做,rv(2)已经是右值了
  receiver(rv());  // receiver的形参成为rv()里的临时变量,然后形参就退化成右值了!!!
  receiver(move(lv));  // 这样就没有任何构造析构了
}

注意

  1. move仍然可以作用于拷贝构造函数,只不过优先级较低,只有没有移动构造函数的情况下采用拷贝构造函数,这时候就把上面例子的移动构造函数变成拷贝构造函数
  2. 移动构造函数只能接受non-const右值,如果有了const右值引用,那么构造函数会用回拷贝构造函数
  3. 不要对右值做move(),因为右值已经是右值了

参考链接

  1. C++11改进我们的程序之右值引用
  2. C++11新特性(66)- 用static_cast将左值转换为右值
  3. C++11 function与bind和move与forward区别引用折叠
  4. Lvalue to rvalue reference binding 由一个小问题,引出了模板的类型推断和引用折叠
  5. google style guide 定义只有移动构造函数和完美转发(perfect forwarding)才能使用右值引用
  6. Rvalue references in Chromium鸿篇巨制
  7. forward

你可能感兴趣的:(C/C++技术,c++)