码迷,mamicode.com
首页 > 编程语言 > 详细

C++ 11 右值引用以及std::move

时间:2015-07-07 00:57:17      阅读:228      评论:0      收藏:0      [点我收藏+]

标签:c++11   移动   右值引用   



        转载请注明出处:http://blog.csdn.net/luotuo44/article/details/46779063



新类型:

         int和int&是什么?都是类型。int是整数类型,int&则是整数引用类型。同样int&&也是一个类型。两个引号&&是C++ 11提出的一个新的引用类型。记住,这是一个新的类型。默念10次吧。如果你记住这个新类型,那么很多疑问都能迎刃而解。并且对《Effective Modern C++》说到的void f(Widget&& w),就很容易明白w是新类型的一个值,肯定是一个左值而不是右值,自然就不用去翻第二页了。

        出现了新类型,就像定义一个新类一样,自然有两件事接着要做:如何初始化、函数匹配(根据参数类型匹配函数)。先看后者。

void fun(int &a)
{
    cout<<"int &a "<<a<<endl;
}

void fun(int &&a)
{
    cout<<"int &&a "<<a<<endl;
}


int main()
{
	int b = 3;
	fun(b);

	return 0;
}

        main函数中的fun(a)会匹配第一个fun函数。因为第二个fun的参数是int右值引用,不能匹配一个左值。值得注意的是,虽然第二个fun函数的a的类型是右值引用类型,但它却是一个左值,因为它是某一个类型变量嘛。

        那要怎么做才能使得b匹配第二个fun函数呢?强制类型转换,把b强制转换成右值引用类型,也就是使用static_cast<int&&>(b)。此时,自然就会匹配第二个fun函数了。

        在C++ 11中,static_cast<T&&>有一个高大上的替代物std::move。其实,高大上的std::move做的事情和前面说的差不多,强制类型转换使得匹配特定的函数而已。

        右值引用和std::move引以自豪的高效率又是怎么实现的呢?本文从经典的拷贝构造函数说起,但例子却不经典。

class Test
{
public:
    Test() : p(nullptr) {}
    ~Test() { delete [] p; }

    Test(Test &t) : p(t.p)//注意这个拷贝构造函数的参数没有const
    {
        t.p = nullptr;//不然会在析构函数中,delete两次p
    }

private:
    char *p;
};

int main()
{
    Test a;
    Test b(a);
    return 0;
}

        注意这个拷贝构造函数的参数没有const

        读者们,你们会觉得上面那个Test在拷贝构造函数不高效吗?几乎是没有任何效率上的负担啊。类似,也能写一个高效的赋值函数。

        但是,一般来说我们的拷贝构造函数的参数都是有const的。有const意味着不能修改参数t,上面的代码也可以看到:将t.p赋值nullptr是必须的。因为t.p不能修改,所以不得不进行深复制,不然将出现经典的浅复制问题。不用说,有const的拷贝构造函数更适合一些,毕竟我们需要从一个const对象中复制一份。


移动构造:


性能的救赎:

        在C++ 11之前,我们只能眼睁睁看着重量级的类只能调用有const的拷贝构造函数,复制一个重量级对象。在C++ 11里面加入了一个新类型右值引用,那能不能用这个右值引用类型作为构造函数的参数呢?当然可以啦。毕竟类的构造函数参数没有什么特别的要求。习惯上,我们会称这样的构造函数为移动(move)构造函数,对应的赋值操作则称为移动(move)赋值函数。他们的代码也很简单,如下:

class Test
{
public:
    Test() : p(nullptr)
    {
        cout<<"constructor"<<endl;
    }

    ~Test()
    {
        delete [] p;
        cout<<"destructor"<<endl;
    }

    Test(const Test& t) : p(nullptr), str(t.str)
    {
        if(t.p != nullptr)
        {
            p = new char[strlen(t.p)+1];
            memcpy(p, t.p, strlen(t.p)+1);
        }

        cout<<"copy constructor"<<endl;
    }

    Test& operator = (const Test& t)
    {
        if( this != &t )
        {
            char *tmp = new char[strlen(t.p)+1];
            memcpy(tmp, t.p, strlen(t.p)+1);
            delete [] p;
            p = tmp;
            str = t.str;
        }

        cout<<"operator = "<<endl;
        return *this;
    }

    Test(Test && t) : p(t.p), str(std::move(t.str))//如何移动由string类完成
    {
        t.p = nullptr;//记得,不然会对同一段内存重复delete
        cout<<"move copy constructor"<<endl;
    }

    Test& operator = (Test &&t)
    {
        if( this != &t)
        {
            p = t.p;
            t.p = nullptr;

            str = std::move(t.str);//如何移动由string类完成
        }

        cout<<"move operator ="<<endl;
        return *this;
    }

private:
    char *p;
    std::string str;
};


协助完成移动构造:

        有了move构造函数和move赋值函数,下一步是协助完成移动构造/移动赋值,包括程序员和编译器。如果不协助的话,可能调用的是copy构造函数而不是move构造函数。从前文也可以看到,协助完成移动构造/移动赋值,其实也就是使得在函数调用时能匹配参数为右值引用的函数。码农能做的就是强制将一个不需要了的对象调用std::move。如下面代码:

int main()
{
	Test a;
	Test b = std::move(a);//调用move构造函数
	Test c = a;//调用copy构造函数
	return 0;
}

        虽然上面的代码在构造b的时候调用了移动构造,但明显上面代码一点都不正常,为什么不直接构造b呢?完全用不着move构造啊。此时可能有读者会想到这样一个用途:我们可以为一个临时对象加上std::move啊,比如operator + 的返回值。实际上这是画蛇添足的。因为编译器会为这个临时对象当作右值(准确说应该是:将亡值),当然也就自动能使用移动构造了。

        难道移动构造是屠龙之技?不是的。移动构造的一大优点是可以高效地在函数中返回一个重量级的类,函数返回值会在后面说到。除了在函数返回值用到外,在函数内部也可以使用到的。

std::vector<std::string> g_ids;//全局变量
void addIds(std::string id)
{
    g_ids.push_back(std::move(id));
}


int main()
{
    addIds("1234");//在添加到g_ids过程中,会调用一次copy构造函数,一次move构造函数
    std::string my_id = "123456789";
    addIds(my_id);//会调用一次copy构造函数,一次move构造函数

    for(auto &e : g_ids)
        cout<<e<<endl;

    return 0;
}

        有读者可能会问,为什么addIds的参数不是const std::string &的形式,这样在对my_id调用的时候就不用为参数id调用一次copy构造函数。但别忘了,此时id被push进g_ids时就要必须要调用一次copy构造函数了。

        前面用红色标出,对一个不需要的了对象调用std::move强制类型转换。为什么说是不需要了的呢?因为一个对象被std::move并且作为move构造函数的参数后,该对象所占用的一些资源可能被移走了,留下一个没有用的空壳。注意,虽然是空壳,但在移动的时候,也要保证这个空壳对象能正确析构。

        或许读者还是觉得移动语义是屠龙之技,那么读者们想一下:vector容器在扩容的时候吧。有了移动语义,vector里面的对象从旧地址搬到新地址,毫不费劲。



右值引用情况下的返回值问题:

        

        有了右值引用,读者可能会写出下面的代码:

Test&& fun()
{
    Test t;
    ...
    return std::move(t);
}


int main()
{
    Test && tt = fun();//和下者,哪个才是正确的呢?
    Test tt = fun();//和上者,哪个才是正确的呢?

    return 0;
}

        无疑,在main函数中,还需要考虑一下tt对象是一个Test类型还是Test&&类型。其实,大错早就在fun函数中铸成了。

        返回的只是一个引用,真身呢?真身已经在fun函数中被摧毁了。Meyers早在《Effective C++》里面就告诫过:不要在函数中返回一个引用。前文也已经说了,右值引用也是一个引用(类型)! 那返回什么好呢? 当然是真身啦!  如同下面代码:

Test fun()
{
    Test t;
    ...

    return t;
}


int main()
{
	Test tt = fun();
	return 0;
}

        当函数返回一个对象时,编译器会将这个对象看作的一个右值(准确来说是将亡值)。所以无需在fun函数中,将return t写成return std::move(t);

        当然,实际上t变量的真身还是在fun函数中被摧毁了,但真身里面有价值的东西都被移走了。对!就像比克大魔王那样,临死前把自己的孩子留下来! 在C++里面,当然不能生成一个孩子,但是可以通过移动构造函数生成一个临时对象,把有价值的东西移走。因为不是移动到main函数的tt变量中,只是移动到了临时对象。所以接下来临时对象还要进行一次移动,把有价值的东西移动到main函数的tt变量中。这个移动过程无疑是一个很好的金蝉脱壳的经典教程。读者可以运行一下代码,可以看到整个移动过程。记住,用g++编译的时候要加入-fno-elide-constructors选项,禁止编译器使用RVO优化。因为这里的RVO优化比移动构造还是省力。所以如果不禁用,会优先使用RVO,而非移动构造函数。


初始化:

        因为右值引用也是一个引用类型,所以只能初始化而不能赋值。既然这样,那只需讨论什么类型的值才能用于初始化一个右值引用。一般来说,右值引用只能引用右值、字面值、将亡值。所以问题转化为:什么是右值?网上介绍的一个方法是:要能不能将取地址符号&应用于某个标识符,如果能就说明它是一个左值,否则为右值。这个方法好像是行得通的。不过,我觉得没有必要分得那么清楚,又不是在考试。在平常写代码时,没有谁会写类似a+++++a这样的考试代码。我个人觉得,记住最常见的那几种就差不多了。比如,字面量(1,‘c‘这类),临时(匿名)对象(即将亡值),经过std::move()转换的对象,函数返回值。其他的右值,还是留给编译器和Scott Meyers吧。如果真的要细究,可以参考stackoverflow上的一个提问《What are rvalues, lvalues, xvalues, glvalues, and prvalues?

        还有一个问题需要说明,const的左值引用(const T&)是一个万能引用,既可以引用左值,也能引用右值。这个是很特殊,特殊得很自然。如果Test类没有定义move构造函数,但用户又使用Test a = std::move(b)构造变量a。那么最终会调用Test类的copy构造函数。一个类的copy构造函数如果用户不定义,编译器会在必要情况下自动合成一个。所以上面的a变量肯定能构造。


谨慎的编译器:

        前一段貌似隐隐约约说到编译器不会自动合成一个move构造函数。是的。如果用户定义了构造函数,copy构造函数,析构函数,operator =中的任何一个,编译器都不会自动为这个类合成一个move构成函数以及move 赋值函数,即使需要用到。我个人觉得是因为,当定义了那四个函数中的任何一个,都可以认为这个类不是nontrival的了。

        想一下,在什么情况下我们是需要析构函数和copy构造函数的。当这个类里面有一些资源(变量)需要我们手动管理的时候。既然有资源要管理,那么读者你觉得编译器默认生成的move构造函数的内部实现应该是怎么样的呢?对类里面的所有成员都调用std::move进行移动?还是调用copy构造函数复制一份呢?这种吃力但又不见得讨好的事情,编译器选择不干。毕竟还有前面说到的const T& 可以引用一个右值。没有move构造函数,copy构造函数顶上即可。

        作为类的设计者,你当然知道那些资源(变量)到底是move还是copy。如果是move的话,那么直接用=default告诉编译器:别担心,直接用对所有变量move就行了。如下:

class Test
{
public:
	Test() p(new int) {}
	~Test()=default;
	Test(const Test&)=delete;
	Test& operator = (const Test&)=delete;

	Test(Test &&)=default;//告诉编译器
	Test& operator = (Test &&)=default;//告诉编译器
	

private:
	std::unique_ptr<int> p;
}






版权声明:本文为博主原创文章,未经博主允许不得转载。

C++ 11 右值引用以及std::move

标签:c++11   移动   右值引用   

原文地址:http://blog.csdn.net/luotuo44/article/details/46779063

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!