【转】c++之右值引用

从左值右值到右值引用

地址:https://www.cnblogs.com/inevermore/p/4029753.html

C++98中规定了左值和右值的概念,但是一般程序员不需要理解的过于深入,因为对于C++98,左值和右值的划分一般用处不大,但是到了C++11,它的重要性开始显现出来。

C++98标准明确规定:
左值是可以取得内存地址的变量。
非左值即为右值。

从这里可以看出,可以执行&取地址的就是左值,其他的就是右值
这里需要明确一点,能否被赋值不是区分C++左值和右值的区别
我们给出四个表达式:

string one("one");
const string two("two");
string three() { return "three"; }
const string four() { return "four"; }

这里四个变量表达式,后面两个是临时变量,不可以取地址,所以属于右值,前面两个就是左值。

这里写出一个函数:

void test(const string &s)
{
    cout << "test(const string &s):" << s << endl;
}
//然后进行测试:
test(one);
test(two);
test(three());
test(four());

编译我们发现,这个test可以接受所有的变量。我们使用另一个函数做测试:

void test(string &s)
{
    cout << "test(string &s):" << s << endl;
}

然后测试发现,只有one可以通过调用。然后我们同时提供两个test函数,然后我们编译运行发现:

test(string &s):one
test(const string &s):two
test(const string &s):three
test(const string &s):four

所以我们可以得出结论:

在C++中,const X&可以接受所有的变量
X &只可以接受普通变量

同时我们可以看出C++重载决议的一个特点:

当一个参数可以匹配多个函数时,总是匹配最精准的一个。

例如上面的one,它也可以接受const X&,但是当X&形式的参数存在时,立刻选择后者。显然后者是专门为其准备的,二者的语义最为符合。X&包含有一种修改语义,这与one是一致的。

引入const属性

上面的四个表达式,我们只讨论了左值和右值,我们再加上const进行讨论。所以:

string one(“one”); 属于非const左值
const string two(“two”); const左值
string three() { return “three”; } 非const右值
const string four() { return “four”; } const右值

左值右值的属性与const是正交的。
现在引入一个问题,如果有时候需要区分四种变量,那么该使用什么方法?
前面的讨论,我们知道X&可以用来区分one,但是剩下的三个都可以被const X&吞掉,显然我们需要为一些变量提供一些定制版本的参数,来让不同的变量优先选择不同的参数。
C++11提供了右值引用来专门区分右值,我们同时提供四个函数:

void test(const string &s)
{
    cout << "test(const string &s):" << s << endl;
}

void test(string &s)
{
    cout << "test(string &s):" << s << endl;
}

void test(string &&s)
{
    cout << "test(string &&s):" << s << endl;
}

void test(const string &&s)
{
    cout << "test(const string &&s):" << s << endl;
}

我们使用C++11进行编译,发现:

test(string &s):one
test(const string &s):two
test(string &&s):three
test(const string &&s):four

我们得出最佳匹配:

X & 匹配 非const左值
const X& 匹配 const左值
X && 匹配 非const右值
const X && 匹配 const右值

然后,我们可以采用逐个函数进行测试,发现:

X& 仅仅匹配 非const左值,这与之前的结论一致
const X& 可以匹配所有的表达式
X && 只可以匹配 非const右值
const X &&可以匹配const和非const 右值

OK,我们的问题解决,当我们需要区分右值的时候,就可以使用右值引用。
事实上,我们一般不需要const X &&,所以我们使用下列三种参数:

void test(string &s); 修改语义
void test(string &&s); 移动语义,后文介绍
void test(const string &s); 常量语义

这三种语义已经足够,在C++98中我们只使用修改和常量语义,在C++11中,正是为了实现移动语义,我们才需要区分右值,才需要引入右值引用。

下文讲C++11右值引用实现移动语义。

右值引用与移动语义

地址:https://www.cnblogs.com/inevermore/p/4029914.html

上节我们提出了右值引用,可以用来区分右值,那么这有什么用处?

问题来源

我们先看一个C++中被人诟病已久的问题:
我把某文件的内容读取到vector中,用函数如何封装?
大部分人的做法是:

void readFile(const string &filename, vector<string> &words)
{
    words.clear();
    //read XXXXX
}

这种做法完全可行,但是代码风格谈不上美观,我们尝试着这样编写:

vector<string> readFile(const string &filename)
{
    vector<string> ret;
    ret.push("cesfwfgw");

    //....
    //

    return ret;
}

这样我们就可以在main中这样调用:
vector<string> coll = readFile("fef.text");

但是,稍微熟悉C++的都知道,这样在语法上会造成大量的开销:

  • ret复制给临时变量,该临时变量开辟在heap上
  • 临时变量复制给coll
  • 这中间产生两次复制和销毁的开销。

如果说这个例子,可以采用开头的代码解决开销,那么如果是一个查询返回结果的函数,那么我们必须这样写:

//这里的开销就无法避免了。
vector<string> queryWord(const string &word)
{
    vector<string> result;
    //XXXXX

    return result;
}

移动语义的引入

我们考虑一个生活中常见的问题(这里参考了如何评价 C++11 的右值引用(Rvalue reference)特性?),如果把一个很重的货物从箱子A移动到箱子B,那么

  • 正常的做法是:打开箱子A,把物品搬出来,移动到B,然后关上A。
  • 另一种比较奇葩的做法是:在B中复制一个物品A,然后将A中的销毁。
  • 更奇葩的做法是:由于复制工具的局限性,我们无法直接在B中复制,所以我们只好先在地上复制一个物品temp,销毁A中的物品,然后根据temp在B中再复制一份,再销毁地上的temp。

事实上,C++98采用的就是最后一种效率极其低下的做法,这里的关键在于,C++98没有很好的区分“copy”和“move”语义
上述问题中,我们明确提出移动A到B中,但是C++98由于移动语义的缺失,只好采用copy的方式完成。
我们再回到开头的问题中:

vector<string> readFile(const string &filename)
{
    vector<string> ret;
    ret.push("cesfwfgw");

    //....
    //

    return ret;
}

这里我们必须看到一点,在完成函数调用后,ret就要被销毁,所以我们想到一个主意,不是把ret中的内容复制给main中的coll,而是将ret中的内容偷到coll中,然后将ret悄悄的销毁。
这样是可行的,因为ret的生命周期很短。

哪些可以偷?

现在问题来了,C++中哪些可以偷,哪些不能?
我们回顾上一节提到的四个表达式:

string one("one");
const string two("two");
string three() { return "three"; }
const string four() { return "four"; }

显然,one和two生命周期较长,不能偷。four具有const属性,拒绝被偷。
那么three是可以被偷取的,因为它是临时变量,又没有const属性。
所以,C++中的非const右值,和移动语义完全匹配。上节我们提出用右值引用区分右值,正是为了解决哪些可以偷的问题!

OK,我们的思路已经很清晰了:

1.为了解决返回对象开销问题,我们提出“偷取”,而不是复制对象
2.我们面临哪些能偷,哪些不能偷的问题。
3.右值可以偷取,所以我们如何区分右值?
4.我们引入右值引用X &&来区分右值。

这就是右值引用的来源。

如果一个变量不是右值,但是我们又需要偷取,那么我们可以采用std::move函数,将其强制转化为右值引用。

例如:

void test(string name)
{
    string temp(std::move(name));
    // XXXXXX
    //**注意,被偷取之后的name无法继续使用,所以move函数不可以随意使用**。
}

带来的影响

那么,右值引用带来哪些改变呢?
首先是类的成员函数赋值,看下面代码:

class People
{
public:
    People() 
    {
        cout << "People()" << endl;
    }
    People(const string &name)
    : name_(name)
    {
        cout << "People(const string &name)" << endl;
    }
    //这里name赋值,我们相对于C++98,提供了一个右值函数,将name的值移动给name_。
    People(string &&name)
    : name_(name)
    {
        cout << "People(string &&name)" << endl;
    }

private:
    string name_;
};

事实上,上面的两个函数可以合成一个:

class People
{
public:
    People() 
    {
        cout << "People()" << endl;
    }

    People(string name)
    : name_(std::move(name))
    {
        cout << "People(string name)" << endl;
    }

private:
    string name_;
};

这里注意,上面的name采用传值,并没有带来开销,因为:

  • 如果name传入的是一个右值,那么name本身采用移动构造,开销比复制小很多,相当于People(string &&name)
  • 如果name传入的其他值,那么name是复制构造,然后移动给name_,也没有增加额外的开销。

对于构造函数,除了提供复制构造函数,还需要移动构造函数。如下:

class People
{
public:
    People() 
    {
        cout << "People()" << endl;
    }

    People(string name)
    : name_(std::move(name))
    {
        cout << "People(string name)" << endl;
    }

    People(const People &p)
    : name_(p.name_)
    {
        cout << "People(const People &p)" << endl;
    }
    People(People &&p)
    : name_(std::move(p.name_))
    {
        cout << "People(People &&p)" << endl;
    }

private:
    string name_;
};

注意在最后一个People(People &&p)中,移动p内的name时,必须显式使用move,来强制移动name成员。

同样,还有移动赋值运算符。

另外,在C++98中,容器内的元素必须具备值语义,现在则不同,元素具备移动能力即可,后文我们在智能指针系列会提到unique_ptr,它可以放入vector中,但是不具有复制和赋值能力。
其他的影响请参考:如何评价 C++11 的右值引用(Rvalue reference)特性?
下文通过一个string的模拟实现,演示右值引用的使用。

使用C++11编写string类以及“异常安全”的=运算符

地址:https://www.cnblogs.com/inevermore/p/4032008.html
前面两节,说明了右值引用和它的作用。下面通过一个string类的编写,来说明右值引用的使用。

相对于C++98,主要是多了移动构造函数和移动赋值运算符

class String
{
public:
    String();
    String(const char *s); //转化语义
    String(const String &s);
    String(String &&s);
    ~String();

    String &operator=(const String &s);
    String &operator=(String &&s);

    friend ostream &operator<<(ostream &os, const String &s)
    {
        return os << s.data_;
    }
private:
    char *data_;
};

下面依次实现每个函数。
第一个是默认构造函数:

String::String()
:data_(new char[1])
{
    *data_ = 0;
    cout << "default" << endl;
}

然后是char*版本的构造函数:

String::String(const char *s)
:data_(new char[strlen(s) + 1])
{
    ::strcpy(data_, s);
    cout << "char *" << endl;
}

重点来了,我们提供移动构造函数:

String::String(String &&s)
:data_(s.data_) //为什么这里可以这样
{
    cout << "move construct" << endl;
    s.data_ = NULL; //防止释放data
}

这里最重要的一点就是要把s的data置为NULL,因为s是个右值,马上就要析构。这样就成功实现了偷取s的内容

析构函数:

String::~String()
{
    delete[] data_;
}

下面我们提供赋值运算符,这里注意一点:
一是处理自我赋值,二是要返回自身引用。

String &String::operator=(const String &s)
{
    if(this != &s)
    {
        delete[] data_;
        data_ = new char[strlen(s.data_) + 1];
        ::strcpy(data_, s.data_);
    }
    return *this;
}

String &String::operator=(String &&s)
{
    if(this != &s)
    {
        cout << "move assignment" << endl;
        delete[] data_;
        data_ = s.data_;
        s.data_ = NULL;
    }
    return *this;
}

后面的移动构造函数,依然要把s的data置为NULL。
上面两个函数看似正确,但是没有处理发生异常的情况,如果new时发生异常,但是此时原本的data已经被delete,造成错误。

如何解决?
我们提供一个swap函数:

void String::swap(String &s)
{
    std::swap(data_, s.data_);
}

一种好的处理方案是:

String &String::operator=(const String &s)
{
    String temp(s);
    swap(temp);

    return *this;
}

String &String::operator=(String &&s)
{
    String temp(s);
    swap(temp);

    return *this;
}

这样,即使生成temp时发生异常,也对自身没有影响。

注意这里没有处理自我赋值,因为自我赋值发生的情况实际比较少,而之前的代码第一行是delete,则必须处理自我赋值。

上面两个赋值运算符可以直接合为一个:

String &String::operator=(String s)
{
    swap(s);

    return *this;
}

事实上,我们在前面也提到过,除了构造函数之外,X &x和X &&类型的函数,可以合二为一为X x,采用传值。
这样,我们的最后一个实现,保证了异常安全。

int main(int argc, char const *argv[])
{
    String s("foo");
    String s2(s);
    //String s3(std::move(String("bar")));
    String s3(String("bar")); //编译器优化 直接使用char*
    cout << s3 << endl;

    s3 = s;
    s3 = String("hello");
    cout << s3 << endl;
    s3 = std::move(s2);
    cout << s3 << endl;

    return 0;
}

注意:

String s3(String(“bar”));

会被编译器优化为

String s3(“bar”)

可以显式使用:

String s3(std::move(String(“bar”)));

猜你喜欢

转载自blog.csdn.net/u013870094/article/details/85938509
今日推荐