C++11的值类别

原创内容,转载请注明出处

Posted by Weakyon Blog on March 29, 2019

在C++11中,除了原本的左值(lvalue),纯右值(rvalue),还加入了一个新的将亡值(xvalue)

本文试图分析以下问题:

  • 为什么要引入移动构造函数
  • 怎么理解左值和纯右值
  • 怎么理解将亡值

一 为什么要引入移动构造函数

C++在C++98/03是不推荐用stl的,因为很多类的效率都很低下

这种效率低下主要体现在对临时值额外的拷贝操作(模板的低效转发也是一方面,本文不讨论这个),因为const T&虽然可以捕获临时值,但是仅仅只是延长了他的生命周期,你只能拷贝他,而不能直接使用它。

看一下使用C++98/03是如何实现一个自己的string的

#include <iostream>
#include <cstring>

class MyString{
public:
    MyString() {std::cout << "default construct" << std::endl}
    MyString(const char* str){
        std::cout << "char construct" << std::endl;
        copyData(str);
    }
    ~MyString() {if (str_) delete(str_);}
    MyString(const MyString &s) {
        std::cout << "copy" << std::endl;
        copyData(s.str_);
    }
    MyString& operator=(const MyString &s) {
        std::cout << "copy operator=" << std::endl;
        if (this != &s) copyData(s.str_);
        return *this;
    }
    friend std::ostream& operator<<(std::ostream& os, const MyString &s) {
        if (s.str_ != nullptr) os << s.str_;
        return os;
    }
private:
    void copyData(const char* str) {
        len_ = std::strlen(str);
        str_ = new char[len_ + 1];
        std::memcpy(str_, str, len_);
        str_[len_] = '\0';
    }
    uint64_t len_ = 0;
    char *str_ = nullptr;
};

int main() {
    MyString a;
    a = "123";
}

输出

default construct
char construct
copy operator=

1 调用了一次默认构造函数用于构造a

2 调用了char为参数的构造函数(调用了一次new,并且复制了一次内存)

3 调用了一次copy(调用了一次new,并且复制了一次内存)

可以看到,只是一次赋值操作,会产生两次new和内存复制。这是因为const MyString&无法判断传入的是一个以后会被使用的左值,还是一个临时的右值

C++11为了填这个坑,新加了移动构造函数,当遇到临时值时,会优先调用移动构造函数。

如下

public:
    MyString(MyString &&s) {
        std::cout << "moved" << std::endl;
        moveData(s.str_, s.len_);
    }
    MyString& operator=(MyString &&s) {
        std::cout << "moved operator=" << std::endl;
        if (this != &s) moveData(s.str_, s.len_);
        return *this;
    }
private:
    void moveData(char* &str, uint64_t &len) {
        len_ = len;
        len = 0;
        str_ = str;
        str = nullptr;
    }

输出

default construct
char construct
moved operator=

1 调用了一次默认构造函数用于构造a

2 调用了char为参数的构造函数(调用了一次new,并且复制了一次内存)

3 调用了一次move(没有new,没有内存拷贝)

可以看到,通过对临时值的判断,就能提高了一倍的性能。

而这种临时值,在C++11中有了新的定义,叫做纯右值

小结

加入移动构造函数是为了优化对临时值(将亡值也能看作是临时值)的识别,从而优化性能

二 怎么理解左值和纯右值

这里直接引用cppreference的定义

  • has identity: it’s possible to determine whether the expression refers to the same entity as another expression, such as by comparing addresses of the objects or the functions they identify (obtained directly or indirectly);
  • can be moved from: move constructor, move assignment operator, or another function overload that implements move semantics can bind to the expression.

In C++11, expressions that:

  • have identity and cannot be moved from are called lvalue expressions;
  • do not have identity and can be moved from are called prvalue (“pure rvalue”) expressions;

两个属性,拥有身份和能被移动

左值是拥有身份和不能被移动的

纯右值是不拥有身份和能被移动的

拥有身份好理解,就是是否可以用&来被取地址(来判断和别的变量是否同一个实体)

但什么是能被移动的?什么是能被移动构造函数绑定的?这就变成先有鸡还是先有蛋的问题了,按我的理解,就是是否能把数据所有权交接给别人的变量

简单来说:

  • 凡是能被&取地址的不能把数据所有权交给别人的,就是左值
  • 凡是临时值,就是右值
  • 凡是字面值(字符串除外),就是右值。(字面值本身是不可被修改的,因此可以move给别人)

cppreference有很多举例,不过最有意思的还是下面例子:

  • i++是右值,而++i是左值

乍一看很奇怪,从操作符重载的角度就很容易理解了

其他的各种操作符的左右值分辨通过这种方式也很容易能够理解

class Time{
public:
    //前置自增返回当前值,可被取地址,是左值
    Time operator++() {
        addSec();
        return *this;
    }
    //后置自增返回临时值,是右值
    Time operator++(int) {
        Time t;
        t = *this;
        addSec();
        return t;
    }
private:
    void addSec() {
        sec_++;
        if (sec_ == 60) {
            sec_ = 0;
            hour_++;
        }
        if (hour_ == 24) {
            hour_ = 0;
        }
    }
    int hour_ = 0;
    int sec_ = 0;
};

三 怎么理解将亡值

分清楚左值和纯右值以后,将亡值就容易理解多了。

左值牢牢把控着对数据的所有权,保证不会被移动构造函数move走

当我们不需要再使用某个左值的时候(常见于各种中间tmp变量),可以用std::move()函数将它强制变成将亡值。

这其实也可以理解成一种临时值,此时会优先匹配移动构造函数。

四 参考文章

C++ 的 std::string 有什么缺点?

Value categories

C++11 中的左值、右值和将亡值

从4行代码看右值引用

29 Mar 2019