C++11右值 &&引用

2023-10-28

C++11右值 &&引用

2017年06月06日 11:56:25 Ink_cherry 阅读数:2394 标签: C++ 右值引用 完美转发

个人分类: C/C++

感觉这篇讲的比较深入细致了,存一下

via:http://www.cnblogs.com/TianFang/archive/2013/01/26/2878356.html

C++ 11 中的右值引用

右值引用的功能

首先,我并不介绍什么是右值引用,而是以一个例子里来介绍一下右值引用的功能:

    #include <iostream>
    #include <vector>
    using namespace std;

    class obj
    {
    public :
        obj() { cout << ">> create obj " << endl; }
        obj(const obj& other) { cout << ">> copy create obj " << endl; }
    };

    vector<obj> foo()
    {
        vector<obj> c;
        c.push_back(obj());

        cout << "---- exit foo ----" << endl;
        return c;
    }

    int main()
    {
        vector<obj> k;
        k = foo();
    }

首先我们编译一下这个函数,运行结果如下:

    tianfang > g++ main.cpp
    tianfang > a.out
    >> create obj 
    >> copy create obj 
    ---- exit foo ----
    >> copy create obj 
    tianfang >

可以看到,对obj对象执行了两次构造。vector是一个常用的容器了,我们可以很容易的分析这这两次拷贝构造的时机:

  1. foo函数第二行,调用push_back的时候,会在vector里建立一个obj的副本

  2. main函数第二行,执行复制函数的时候,会把foo()返回的对象全部复制过来,再次执行一次拷贝构造

由于对象的拷贝构造的开销是非常大的,因此我们想就可能避免他们。其中,第一次拷贝构造是vector的特性所决定的,不可避免。但第二次拷贝构造,在C++ 11中就是可以避免的了。

    tianfang > g++ -std=c++11 main.cpp
    tianfang > a.out
    >> create obj 
    >> copy create obj 
    ---- exit foo ----
    tianfang >

可以看到,我们除了加上了一个-std=c++11选项外,什么都没干,但现在就把第二次的拷贝构造给去掉了。它是如何实现这一过程的呢?

在老版本中,当我们执行第二行的赋值操作的时候,执行过程如下:

  1. foo()函数返回一个临时对象(这里用~tmp来标识它)

  2. 执行vector的 '=' 函数,将对象k中的现有成员删除,将~tmp的成员复制到k中来

  3. 删除临时对象~tmp

在C++11的版本中,执行过程如下:

  1. foo()函数返回一个临时对象(这里用~tmp来标识它)

  2. 执行vector的 '=' 函数,将对象k中的成员~tmp的成员互换,此时k中的成员就被替换成了~tmp中的成员。

  3. 删除临时对象~tmp(此时就删除了以前的k中的成员)

关键的过程就是第2步,它不是复制而是交换,从而避免的成员的拷贝,但效果却是一样的。不用修改代码,性能却得到了提升,对于程序员来说就是一份免费的午餐。

但是,这份免费的午餐也不是无条件就可以获取的,带上-std=c++11编译时,如果使用STL代码可以享用这份午餐,但如果使用我们以前的老代码发现还是和以前的功能是一样的,那么,如何让我们以前的代码也能得到这个效率的提升呢?

   

通过交换减少数据的拷贝

为了演示如何在我们的代码中也获取这个性能提升,首先我先写了一个山寨的vector:

    #include <iostream>
    #include <vector>
    using namespace std;

    class obj
    {
    public :
        obj() { cout << ">> create obj " << endl; }
        obj(const obj& other) { cout << ">> copy create obj " << endl; }
    };

    template <class T>
    class container
    {
    public:
        T* value;

    public:
        container() : value(NULL) {};
        ~container() { delete value; } 

        container(const container& other)
        {
            value = new T(*other.value);
        }

        const container& operator = (const container& other)
        {
            delete value;
            value = new T(*other.value);
            return *this;
        }

        void push_back(const T& item)
        {
            delete value;
            value = new T(item);
        }
    };

    container<obj> foo()
    {
        container<obj> c;
        c.push_back(obj());

        cout << "---- exit foo ----" << endl;
        return c;
    }

    int main()
    {
        container<obj> k ;
        k = foo();    
    }

这个vector只能容纳一个元素,但并不妨碍我们的演示,其功能和前面的例子是一样的,运行这段代码,结果如下:

    tianfang > make
    g++ -std=c++11 main.cpp
    tianfang > a.out
    >> create obj 
    >> copy create obj 
    ---- exit foo ----
    >> copy create obj 
    tianfang >

如前所述,仍然有两次拷贝构造。其实前面已经说过交换实现减少拷贝构造的原理,那么,我们可以通过修改 '=' 函数来手动实现这一过程。

    const container& operator = (container& other)
    {
        T* tmp = value;
        value = other.value;
        other.value = tmp;
        return *this;
    }

在VC中运行这段代码,发现运行结果和预期一致,

    >> create obj 
    >> copy create obj 
    ---- exit foo ----

但是,gcc中却无法通过编译,原因很简单:gcc期望的赋值函数的参数是const型的,而这里为了交换成员,而不能使用const型。

那么,虽然gcc中不能生效,是否可以说在vc中就可以以这种形式获取性能提升呢?答案是否定的。虽然在这段代码中这么写没有问题,但赋值函数本身是期望复制功能的,而不是交换。例如,修改后下面的运行结果就不对了。

    int main()
    {
        container<obj> k, k2;
        k = foo();    

        //预期结果是复制,但执行了交换
        k2 = k;
    }

gcc的告警是有道理的:如果 '=' 函数实现的是复制功能,虽然效率低点,但保证了功能正确,但如果实现的是交换的功能,则不能保证功能一定正确。只有当 '=' 函数右边的对象为一个临时变量的时候,由于临时变量会马上被删除掉,此时的交换和复制的效果是一样的。其实VC也应该把这个告警加上才合适。

PS:对临时变量定义和来源不清楚的朋友可以参考一下这篇文章

现在的问题是:我们无法在赋值函数里区分传入的是一个临时对象还是非临时对象,因此只能执行复制操作。为了解决这一问题,c++中引入了一个新的赋值函数的重载形式:

    container& operator = (container&& other)

这个赋值函数通常称为移动赋值函数,和老版本的相比,它有两点区别:

  1. 入参不是const型,因此它是可以更改入参的值的,从而实现交换操作

  2. 入参前面有两个&号,这个是C++11引入的新语法,称为右值引用,它的使用方式和普通引用是一样的,唯一的区别是可以指向临时变量。

现在,我们就有两个版本的赋值函数了,C++11在语法级别也做了适应:

  • 如果入参是临时变量,则执行移动赋值函数,如果没有定义移动赋值函数,则执行复制赋值函数(以保证老版本代码能编译通过)

  • 如果入参不是临时变量,则执行普通的复制赋值函数

现在,我们实现一下山寨版的移动赋值函数:

    container& operator = (container&& other)
    {
        delete value;
        value = other.value;
        other.value = NULL;
        return *this;

    }

运行后结果就和我们期望的那样,避免了成员的第二次的拷贝构造。

和移动赋值函数相应的,也有一个一个移动构造函数,也最好实现以下:

    container (container&& other)
    {
        value = other.value;
        other.value = NULL;
    }

我们也可以实现自己的右值引用版的重载函数,这里就不多介绍了。

注意:本文所示的代码只是为了演示和实现右值引用,力求简洁,并没有写得很完善(一个典型的缺失就是在赋值函数中没有判断入参是否是本身),请不要将其应用于项目中。

完善的版本请看MSDN文章:如何编写一个移动构造函数,其相应的对右值引用的介绍文章Rvalue引用声明:&&也非常值得一读。

 

通过std::move函数显式使用交换

首先看一下这段代码:

    class bigobj
    {
    public :
        bigobj() { cout << ">> create obj " << endl; }
        bigobj(const bigobj& other) { cout << ">> copy create obj " << endl; }
        bigobj(bigobj&& other) { cout << ">> move create obj " << endl; }
    };

    int main()
    {
        list<bigobj> list;
        for(int i = 0; i < 3; i++)
        {
            bigobj obj;
            list.push_back(obj);
        }
    }

运行的时候就会发现:虽然我们定义了移动构造函数,但是它仍然会执行拷贝构造函数。这是因为编译器并不认为obj是临时变量。关于什么变量才是临时变量,前文已经给了个链接来说明它,简单的说,我们能够看到的命名变量都不是临时变量。

虽然obj对象不是语言级别的临时变量,但是从功能上来看,它就是一个临时变量,是可以使用移动构造函数来消除拷贝带来的性能损失的。为了解决这一问题,C++提供了一个move函数来把obj变量强制转换为右值引用,这样就可以使用移动构造函数了。

    for(int i = 0; i < 3; i++)
    {
        bigobj obj;
        list.push_back(std::move(obj));
    }

不过,需要注意的是,和系统识别的临时变量而自动使用右值引用不同,这种强制转换是有一定的风险的,由于在push_back后执行了交换操作,如果再次使用它会现非预期的结果,只有能确定该变量不会再次被使用才能执行这种转换。

-----------------------------------------------------------------------------------------------------

另一篇

 

C++0x标准来很长时间了,引入了很多牛逼的特性[1]。其中一个便是右值引用,Thomas Becker的文章[2]很全面的介绍了这个特性,读后有如醍醐灌顶,翻译在此以便深入理解。

目录

  1. 概述
  2. move语义
  3. 右值引用
  4. 强制move语义
  5. 右值引用是右值吗?
  6. move语义与编译器优化
  7. 完美转发:问题
  8. 完美转发:解决方案
  9. Rvalue References And Exceptions
  10. The Case of the Implicit Move
  11. Acknowledgments and Further Reading

概述

右值引用是由c++0x标准引入c++的一个令人难以捉摸的特性。我曾偶尔听到过有c++领域的大牛这么说:

每次我想抓住右值引用的时候,它总能从我手里跑掉。

想把右值引用装进脑袋实在太难了。

我不得不教别人右值引用,这太可怕了。

右值引用恶心的地方在于,当你看到它的时候根本不知道它的存在有什么意义,它是用来解决什么问题的。所以我不会马上介绍什么是右值引用。更好的方式是从它将解决的问题入手,然后讲述右值引用是如何解决这些问题的。这样,右值引用的定义才会看起来合理和自然。

右值引用至少解决了这两个问题:

  1. 实现move语义
  2. 完美转发(Perfect forwarding)

如果你不懂这两个问题,别担心,后面会详细地介绍。我们会从move语义开始,但在开始之前要首先让你回忆起c++的左值和右值是什么。关于左值和右值我很难给一个严密的定义,不过下面的解释已经足以让你明白什么是左值和右值。

C语言发展的较早时期,左值和右值的定义是这样的:左值是一个可以现在赋值运算符的左边或者右边的表达式e,而右值则是只能现在右边的表达式。例如:

int a = 42;                                                
int b = 43;                                                

// a与b都是左值                              
a = b; // ok                                                
b = a; // ok                                                
a = a * b; // ok                                            

// a * b是右值:                                      
int c = a * b; // ok, 右值在等号右边
a * b = 42; // 错误,右值在等号左边
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

在c++中,我们仍然可以用这个直观的办法来区分左值和右值。不过,c++中的用户自定义类型引入了关于可变性和可赋值性的微妙变化,这会让这个方法变的不那么地正确。我们没有必要继续深究下去,这里还有另外一种定义可以让你很好的处理关于右值的问题:左值是一个指向某内存空间的表达式,并且我们可以用&操作符获得该内存空间的地址。右值就是非左值的表达式。例如:

// 左值:                                                        
//                                                                
int i = 42;                                                        
i = 43; // ok, i是左值
int* p = &i; // ok, i是左值
int& foo();                                                        
foo() = 42; // ok, foo()是左值
int* p1 = &foo(); // ok, foo()是左值

// 右值:                                                        
//                                                                
int foobar();                                                      
int j = 0;                                                        
j = foobar(); // ok, foobar()是右值
int* p2 = &foobar(); // 错误,不能取右值的地址
j = 42; // ok, 42是右值
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

如果你对左值和右值的严密的定义有兴趣的话,可以看下Mikael Kilpel�0�1inen的文章[3]。

move语义

假设class X包含一个指向某资源的指针或句柄m_pResource。这里的资源指的是任何需要耗费一定的时间去构造、复制和销毁的东西,比如说以动态数组的形式管理一系列的元素的std::vector。逻辑上而言X的赋值操作符应该像下面这样:

X& X::operator=(X const & rhs)
{
  // [...]
  // 销毁m_pResource指向的资源
  // 复制rhs.m_pResource所指的资源,并使m_pResource指向它
  // [...]
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

同样X的拷贝构造函数也是这样。假设我们这样来用X:

X foo(); // foo是一个返回值为X的函数
X x;
x = foo();
  • 1
  • 2
  • 3
  • 1
  • 2
  • 3

最后一行有如下的操作:

  1. 销毁x所持有的资源
  2. 复制foo返回的临时对象所拥有的资源
  3. 销毁临时对象,释放其资源

上面的过程是可行的,但是更有效率的办法是直接交换x和临时对象中的资源指针,然后让临时对象的析构函数去销毁x原来拥有的资源。换句话说,当赋值操作符的右边是右值的时候,我们希望赋值操作符被定义成下面这样:

// [...]
// swap m_pResource and rhs.m_pResource
// [...]
  • 1
  • 2
  • 3
  • 1
  • 2
  • 3

这就是所谓的move语义。在之前的c++中,这样的行为是很难实现的。虽然我也听到有的人说他们可以用模版元编程来实现,但是我还从来没有遇到过能给我解释清楚如何具体实现的人。所以这一定是相当复杂的。C++0x通过重载的办法来实现:

X& X::operator=(<mystery type> rhs)
{
  // [...]
  // swap this->m_pResource and rhs.m_pResource
  // [...]  
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

既然我们是要重载赋值运算符,那么<mystery type>肯定是引用类型。另外我们希望<mystery type>具有这样的行为:现在有两种重载,一种参数是普通的引用,另一种参数是<mystery type>,那么当参数是个右值时就会选择<mystery type>,当参数是左值是还是选择普通的引用类型。

把上面的<mystery type>换成右值引用,我们终于看到了右值引用的定义。

右值引用

如果X是一种类型,那么X&&就叫做X的右值引用。为了更好的区分两,普通引用现在被称为左值引用。

右值引用和左值引用的行为差不多,但是有几点不同,最重要的就是函数重载时左值使用左值引用的版本,右值使用右值引用的版本:

void foo(X& x); // 左值引用重载
void foo(X&& x); // 右值引用重载

X x;
X foobar();

foo(x); // 参数是左值,调用foo(X&)
foo(foobar()); // 参数是右值,调用foo(X&&)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

重点在于:

右值引用允许函数在编译期根据参数是左值还是右值来建立分支。

理论上确实可以用这种方式重载任何函数,但是绝大多数情况下这样的重载只现在拷贝构造函数和赋值运算符中,以用来实现move语义:

X& X::operator=(X const & rhs); // classical implementation
X& X::operator=(X&& rhs)
{
  // Move semantics: exchange content between this and rhs
  return *this;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

实现针对右值引用重载的拷贝构造函数与上面类似。

如果你实现了void foo(X&);,但是没有实现void foo(X&&);,那么和以前一样foo的参数只能是左值。如果实现了void foo(X const &);,但是没有实现void foo(X&&);,仍和以前一样,foo的参数既可以是左值也可以是右值。唯一能够区分左值和右值的办法就是实现void foo(X&&);。最后,如果只实现了实现void foo(X&&);,但却没有实现void foo(X&);void foo(X const &);,那么foo的参数将只能是右值。

强制move语义

c++的第一版修正案里有这样一句话:“C++标准委员会不应该制定一条阻止程序员拿起枪朝自己的脚丫子开火的规则。”严肃点说就是c++应该给程序员更多控制的权利,而不是擅自纠正他们的疏忽。于是,按照这种思想,C++0x中既可以在右值上使用move语义,也可以在左值上使用,标准程序库中的函数swap就是一个很好的例子。这里假设X就是前面我们已经重载右值引用以实现move语义的那个类。

template<class T>
void swap(T& a, T& b)
{
  T tmp(a);
  a = b;
  b = tmp;
}

X a, b;
swap(a, b);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

上面的代码中没有右值,所以没有使用move语义。但move语义用在这里最合适不过了:当一个变量(a)作为拷贝构造函数或者赋值的来源时,这个变量要么就是以后都不会再使用,要么就是作为赋值操作的目标(a = b)。

C++11中的标准库函数std::move可以解决我们的问题。这个函数只会做一件事:把它的参数转换为一个右值并且返回。C++11中的swap函数是这样的:

template<class T>
void swap(T& a, T& b)
{
  T tmp(std::move(a));
  a = std::move(b);
  b = std::move(tmp);
}

X a, b;
swap(a, b);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

现在的swap使用了move语义。值得注意的是对那些没有实现move语义的类型来说(没有针对右值引用重载拷贝构造函数和赋值操作符),新的swap仍然和旧的一样。

std::move是个很简单的函数,不过现在我还不能将它的实现展现给你,后面再详细说明。

像上面的swap函数一样,尽可能的使用std::move会给我们带来以下好处:

  • 对那些实现了move语义的类型来说,许多标准库算法和操作会得到很大的性能上的提升。例如就地排序:就地排序算法基本上只是在交换容器内的对象,借助move语义的实现,交换操作会快很多。
  • stl通常对某种类型的可复制性有一定的要求,比如要放入容器的类型。其实仔细研究下,大多数情况下只要有可移动性就足够了。所以我们可以在一些之前不可复制的类型不被允许的情况下,用一些不可复制但是可以移动的类型(unique_ptr)。这样的类型是可以作为容器元素的。

右值引用是右值吗?

假设有以下代码:

void foo(X&& x)
{
  X anotherX = x;
  // ...
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 1
  • 2
  • 3
  • 4
  • 5

现在考虑一个有趣的问题:在foo函数内,哪个版本的X拷贝构造函数会被调用呢?这里的x是右值引用类型。把x也当作右值来处理看起来貌似是正确的,也就是调用这个拷贝构造函数:

X(X&& rhs);
  • 1
  • 1

有些人可能会认为一个右值引用本身就是右值。但右值引用的设计者们采用了一个更微妙的标准:

右值引用类型既可以被当作左值也可以被当作右值,判断的标准是,如果它有名字,那就是左值,否则就是右值。

在上面的例子中,因为右值引用x是有名字的,所以x被当作左值来处理。

void foo(X&& x)
{
  X anotherX = x; // 调用X(X const & rhs)
}
  • 1
  • 2
  • 3
  • 4
  • 1
  • 2
  • 3
  • 4

下面是一个没有名字的右值引用被当作右值处理的例子:

X&& goo();
X x = goo(); // 调用X(X&& rhs),goo的返回值没有名字
  • 1
  • 2
  • 1
  • 2

之所以采用这样的判断方法,是因为:如果允许悄悄地把move语义应用到有名字的东西(比如foo中的x)上面,代码会变得容易错和让人迷惑。

void foo(X&& x)
{
  X anotherX = x;
  // x仍然在作用域内
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 1
  • 2
  • 3
  • 4
  • 5

这里的x仍然是可以被后面的代码所访问到的,如果把x作为右值看待,那么经过X anotherX = x;后,x的内容已经发生变化。move语义的重点在于将其应用于那些不重要的东西上面,那些move之后会马上销毁而不会被再次用到的东西上面。所以就有了上面的准则:如果有名字,那么它就是左值。

那另外一半,“如果没有名字,那它就是右值”又如何理解呢?上面goo()的例子中,理论上来说goo()所引用的对象也可能在X x = goo();后被访问的到。但是回想一下,这种行为不正是我们想要的吗?我们也想随心所欲的在左值上面使用move语义。正是“如果没有名字,那它就是右值”的规则让我们能够实现强制move语义。其实这就是std::move的原理。这里展示std::move的具体实现还是太早了点,不过我们离理解std::move更近了一步。它什么都没做,只是把它的参数通过右值引用的形式传递下去。

std::move(x)的类型是右值引用,而且它也没有名字,所以它是个右值。因此std::move(x)正是通过隐藏名字的方式把它的参数变为右值。

下面这个例子将展示记住“如果它有名字”的规则是多么重要。假设你写了一个类Base,并且通过重载拷贝构造函数和赋值操作符实现了move语义:

Base(Base const & rhs); // non-move semantics
Base(Base&& rhs); // move semantics
  • 1
  • 2
  • 1
  • 2

然后又写了一个继承自Base的类Derived。为了保证Derived对象中的Base部分能够正确实现move语义,必须也重载Derived类的拷贝构造函数和赋值操作符。先让我们看下拷贝构造函数(赋值操作符的实现类似),左值版本的拷贝构造函数很直白:

Derived(Derived const & rhs)
  : Base(rhs)
{
  // Derived-specific stuff
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 1
  • 2
  • 3
  • 4
  • 5

但右值版本的重载却要仔细研究下,下面是某个不知道“如果它有名字”规则的程序员写的:

Derived(Derived&& rhs)
  : Base(rhs) // 错误:rhs是个左值
{
  // ...
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 1
  • 2
  • 3
  • 4
  • 5

如果像上面这样写,调用的永远是Base的非move语义的拷贝构造函数。因为rhs有名字,所以它是个左值。但我们想要调用的却是move语义的拷贝构造函数,所以应该这么写:

Derived(Derived&& rhs)
  : Base(std::move(rhs)) // good, calls Base(Base&& rhs)
{
  // Derived-specific stuff
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 1
  • 2
  • 3
  • 4
  • 5

move语义与编译器优化

现在有这么一个函数:

X foo()
{
  X x;
  // perhaps do something to x
  return x;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

一看到这个函数,你可能会说,咦,这个函数里有一个复制的动作,不如让它使用move语义:

X foo()
{
  X x;
  // perhaps do something to x
  return std::move(x); // making it worse!
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

很不幸的是,这样不但没有帮助反而会让它变的更糟。现在的编译器基本上都会做返回值优化(return value optimization)。也就是说,编译器会在函数返回的地方直接创建对象,而不是在函数中创建后再复制来。很明显,这比move语义还要好一点。

所以,为了更好的使用右值引用和move语义,你得很好的理解现在编译器的一些特殊效果,比如return value optimization和copy elision。并且在运用右值引用和move语义时将其考虑在内。Dave Abrahams就这一主题写了一系列的文章[4]。

完美转发:问题

除了实现move语义之外,右值引用要解决的另一个问题就是完美转发问题(perfect forwarding)。假设有下面这样一个工厂函数:

template<typename T, typename Arg>
shared_ptr<T> factory(Arg arg)
{
  return shared_ptr<T>(new T(arg));
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 1
  • 2
  • 3
  • 4
  • 5

很明显,这个函数的意图是想把参数arg转发给T的构造函数。对参数arg而言,理想的情况是好像factory函数不存在一样,直接调用构造函数,这就是所谓的“完美转发”。但真实情况是这个函数是错误的,因为它引入了额外的通过值的函数调用,这将不适用于那些以引用为参数的构造函数。

最常见的解决方法,比如被boost::bind采用的,就是让外面的函数以引用作为参数。

template<typename T, typename Arg>
shared_ptr<T> factory(Arg& arg)
{
  return shared_ptr<T>(new T(arg));
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 1
  • 2
  • 3
  • 4
  • 5

这样确实会好一点,但不是完美的。现在的问题是这个函数不能接受右值作为参数:

factory<X>(hoo()); // error if hoo returns by value
factory<X>(41); // error
  • 1
  • 2
  • 1
  • 2

这个问题可以通过一个接受const引用的重载解决:

template<typename T, typename Arg>
shared_ptr<T> factory(Arg const & arg)
{
  return shared_ptr<T>(new T(arg));
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 1
  • 2
  • 3
  • 4
  • 5

这个办法仍然有两个问题。首先如果factory函数的参数不是一个而是多个,那就需要针对每个参数都要写const引用和non-const引用的重载。代码会变的奇的长。

其次这种办法也称不上是完美转发,因为它不能实现move语义。factory内的构造函数的参数是个左值(因为它有名字),所以即使构造函数本身已经支持,factory也无法实现move语义。

右值引用可以很好的解决上面这些问题。它使得不通过重载而实现真正的完美转发成为可能。为了弄清楚是如何实现的,我们还需要再掌握两个右值引用的规则。

完美转发:解决方案

第一条右值引用的规则也会影响到左值引用。回想一下,在c++11标准之前,是不允许现对某个引用的引用的:像A& &这样的语句会导致编译错误。不同的是,在c++11标准里面引入了引用叠加规则:

A& & => A&
A& && => A&
A&& & => A&
A&& && => A&&
  • 1
  • 2
  • 3
  • 4
  • 1
  • 2
  • 3
  • 4

另外一个是模版参数推导规则。这里的模版是接受一个右值引用作为模版参数的函数模版。

template<typename T>
void foo(T&&);
  • 1
  • 2
  • 1
  • 2

针对这样的模版有如下的规则:

  1. 当函数foo的实参是一个A类型的左值时,T的类型是A&。再根据引用叠加规则判断,最后参数的实际类型是A&。
  2. 当foo的实参是一个A类型的右值时,T的类型是A。根据引用叠加规则可以判断,最后的类型是A&&。

有了上面这些规则,我们可以用右值引用来解决前面的完美转发问题。下面是解决的办法:

template<typename T, typename Arg>
shared_ptr<T> factory(Arg&& arg)
{
  return shared_ptr<T>(new T(std::forward<Arg>(arg)));
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 1
  • 2
  • 3
  • 4
  • 5

std::forward的定义如下:

template<class S>
S&& forward(typename remove_reference<S>::type& a) noexcept
{
  return static_cast<S&&>(a);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 1
  • 2
  • 3
  • 4
  • 5

上面的程序是如何解决完美转发的问题的?我们需要讨论当factory的参数是左值或右值这两种情况。假设A和X是两种类型。先来看factory的参数是X类型的左值时的情况:

X x;
factory<A>(x);
  • 1
  • 2
  • 1
  • 2

根据上面的规则可以推导得到,factory的模版参数Arg变成了X&,于是编译器会像下面这样将模版实例化:

shared_ptr<A> factory(X& && arg)
{
  return shared_ptr<A>(new A(std::forward<X&>(arg)));
}

X& && forward(remove_reference<X&>::type& a) noexcept
{
  return static_cast<X& &&>(a);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

应用前面的引用叠加规则并且求得remove_reference的值后,上面的代码又变成了这样:

shared_ptr<A> factory(X& arg)
{
  return shared_ptr<A>(new A(std::forward<X&>(arg)));
}

X& std::forward(X& a)
{
  return static_cast<X&>(a);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

这对于左值来说当然是完美转发:通过两次中转,参数arg被传递给了A的构造函数,这两次中转都是通过左值引用完成的。

现在再考虑参数是右值的情况:

X foo();
factory<A>(foo());
  • 1
  • 2
  • 1
  • 2

再次根据上面的规则推导得到:

shared_ptr<A> factory(X&& arg)
{
  return shared_ptr<A>(new A(std::forward<X>(arg)));
}

X&& forward(X& a) noexcept
{
  return static_cast<X&&>(a);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

对右值来说,这也是完美转发:参数通过两次中转被传递给A的构造函数。另外对A的构造函数来说,它的参数是个被声明为右值引用类型的表达式,并且它还没有名字。那么根据第5节中的规则可以判断,它就是个右值。这意味着这样的转发完好的保留了move语义,就像factory函数并不存在一样。

事实上std::forward的真正目的在于保留move语义。如果没有std::forward,一切都是正常的,但有一点除外:A的构造函数的参数是有名字的,那这个参数就只能是个左值。

如果你想再深入挖掘一点的话,不妨问下自己这个问题:为什么需要remove_reference?答案是其实根本不需要。如果把remove_reference<S>::type&换成S&,一样可以得和上面相同的结论。但是这一切的前提是我们指定Arg作为std::forward的模版参数。remove_reference存在的原因就是强迫我们去这样做。

已经讲的差不多了,剩下的就是std::move的实现了。记住,std::move的用意在于将它的参数传递下去,将它转换成右值。

template<class T>
typename remove_reference<T>::type&&
std::move(T&& a) noexcept
{
  typedef typename remove_reference<T>::type&& RvalRef;
  return static_cast<RvalRef>(a);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

下面假设我们针对一个X类型的左值调用std::move。

X x;
std::move(x);
  • 1
  • 2
  • 1
  • 2

根据前面的模版参数推导规则,模版参数T变成了X&,于是:

typename remove_reference<X&>::type&&
std::move(X& && a) noexcept
{
  typedef typename remove_reference<X&>::type&& RvalRef;
  return static_cast<RvalRef>(a);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

然后求得remove_reference的值,并应用引用叠加规则,得到:

X&& std::move(X& a) noexcept
{
  return static_cast<X&&>(a);
}
  • 1
  • 2
  • 3
  • 4
  • 1
  • 2
  • 3
  • 4

这就可以了,x变成了没有名字的右值引用。

参数是右值的情况由你来自己推导。不过你可能马上就想跳过去了,为什么会有人把std::move用在右值上呢?它的功能不就是把参数变成右值么。另外你可能也注意到了,我们完全可以用static_cast<X&&>(x)来代替std::move(x),不过大多数情况下还是用std::move(x)比较好。

参考

  1. C++11 from wikipedia
  2. C++ Rvalue References Explained
  3. Lvalues and Rvalues
  4. RValue References: Moving Forward�0�3
  5. A Brief Introduction to Rvalue References
  6. C++11 标准新特性: 右值引用与转移语义
  7. C++11 完美转发
  8. 《C++0x漫谈》系列之:右值引用(或“move语意与完美转发”)(下)

本文转自:[译]详解C++右值引用

——————————————————————————关于引用叠加规则函数例子via:http://shaoyuan1943.github.io/2016/03/26/explain-move-forward/

#include<string>
#include<cstring>
#include<iostream>
using namespace std;
template <typename T>
struct Name;

template <>
struct Name<string>
{
	static const char * get()
	{
		return "string";
	}
};

template <>
struct Name<const string>
{
	static const char * get()
	{
		return "const string";
	}
};

template <>
struct Name<string&>
{
	static const char * get()
	{
		return "string&";
	}
};

template <>
struct Name<const string&>
{
	static const char * get()
	{
		return "const string&";
	}
};

template <>
struct Name<string&&>
{
	static const char * get()
	{
		return "string&&";
	}
};

template <>
struct Name<const string&&>
{
	static const char * get()
	{
		return "const string&&";
	}
};

template <typename T>
void quark(T&& t)
{
	cout << "**********************************" << endl;
	cout << "t: " << t << endl;
	cout << "T: " << Name<T>::get() << endl;  // -->A
	cout << "T&&: " << Name<T&&>::get() << endl;  // -->B
	cout << endl;
}

string strange()          
{
	return "strange()";
}

const string charm()
{
	return "charm()";
}


int main()
{
	string up("up");           
	const string down("down"); 
	quark(up);	// -->1
	quark(down);	// -->2
	quark(strange());	// -->3
	quark(charm());		// -->4
	return 0;
}

 

  1. 在调用quack时,up被推导为string&类型。在quack中,T会被推导为string&类型,根据 T& + && -> T&规则,也就是说进入到quack中,T是string&,所以A调用了string&的版本。仍然是上述规则,在执行B时,T依旧被转换和推导为string&,所以就走到了相应的版本中。
  2. 因为down具有const属性,所以表现形式与1一样,唯一的区别是A和B均调用到具有const属性的版本。
  3. 这里有点特殊,因为strange()返回的是临时对象,类型为string,因此进入quack之后,T仍旧为string类型,因此A最终会进入string的调用版本,同理T&&就是string&&,会进入对应的版本中。
  4. 这里有点特殊,因为charm()返回的是临时对象,类型为string,因此进入quack之后,T为string类型,由于有const属性,所以A和B都会调用到const属性的版本。

上面的代码中我们给了std::forward的大致实现,其实说白了,就是利用引用折叠规则保留参数原始类型,拒绝编译器的类型推导,以达到将参数完美转发到目的函数中。

之所有存在完美转发,其问题实质是:模板参数类型推导在转发过程中无法保证左右值的引用问题。而完美转发就是在不破坏const属性的前提下通过增加左右值引用概念和新增参数推导规则解决这个问题。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

C++11右值 &&引用 的相关文章

  • CSS列表与表格

    目录 编辑 HTML 列表和 CSS 列表属性 不同的列表项目标记 实例 图像作为列表项标记 实例 定位列表项标记 实例 删除默认设置 实例 列表 简写属性 实例 设置列表的颜色样式 实例 更多实例 所有 CSS 列表属性 表格边框 实例
  • python PDF转docx库的安装与使用

    python PDF转docx库的安装与使用 下载 使用 三种方法 效果 下载 anaconda中直接通过anaconda prompt下载pdf2docx库 pip install pdf2docx 使用 三种方法 可以直接通过命令行使用
  • SpringMVC基础(5): SpringMVC常用注解

    目录 1 handler method 参数绑定常用注解 2 常用注解 1 PathVariable 2 RequestHeader CookieValue 3 Re Body 5 RequestBody和 RequestParam区别 6
  • 安装部署mysql、redis、minio、nginx

    离线安装部署mysql redis minio nginx jar包启动 系统环境 centos7 9 架构 X86 安装目录 server 记录安装笔记 依赖项在线安装的 下载离线依赖 yum install y nginx downlo
  • 最新升级的STM32CubeIDE属于一站式工具,本文带你体验它的强大

    目录 下载安装 配置生成代码 硬件在线调试 1 写在前面 2 STM32CubeIDE介绍 主要特点 3 STM32CubeIDE下载 4 STM32CubeMX安装 5创建工程并编译 下载安装 配置生成代码 硬件在线调试 1 写在前面 S
  • Vue 如何使用WebSocket与服务器建立链接 持续保持通信

    WebSocket 浏览器通过JavaScript向服务器发出建立WebSocket链接的请求 链接建立后 客户端和服务器端就可以通过TCP链接直接交互数据 WebSocket链接后可以通过send 方法来向服务器发送数据 并通过onnes
  • 【nodejs课堂】Node.js 创建第一个应用

    在我们创建Node js第一个 hello world 应用前 先了解一下Node js应用由哪几部分组成 1 引入required模块 我们可以使用require指令来载入Node js模块 2 创建服务器 服务器可以监听客户端的请求 类
  • MyEclipse8 GA 下载地址 注册码 优化指南

    官方网站限制大陆IP 可以通过http ajava org 直接下载 下面是winodws linux的下载地址 其他版本请自行去http ajava org 下载 注册码7 5GA 8 0GA貌似通用 但不绝对 window版下载地址 h
  • 语义分割数据增强(Data augmentation for semantic segmentation)

    数据增强 深度学习模型的鲁棒性 robustness 和泛化性受到训练数据的多样性和数据量所影响 数据增强 data augmentation 是机器学习和深度学习中经常采用的一个方法 其目的是扩大训练样本的数量 语义分割是计算机视觉一个重
  • 期货十三篇 第七篇 平仓篇

    在期货交易中曾经流行这样一句话 会开仓的是徒弟 会平仓的是师傅 会空仓的是大师傅 在最初的期货交易过程中 我的开仓还算比较顺利 基本上都可以获得一定的盈利 但是我的平仓却十分糟糕 往往把自己的盈利单变成微利单或者亏损单而狼狈离场 我实在不能
  • 贝锐蒲公英:助力企业打造稳定高效的智能安防监控网络

    随着技术的快速发展和物联网的普及 企业面临着许多安全威胁和风险 如盗窃 入侵 信息泄露等 企业需要建立安防监控系统来保护其资产 员工和业务运营的安全 然而 企业在搭建安防监控系统的过程中 可能会面临一些难点 包括以下几个方面 1 监控设备多
  • sysdig_系统分析工具 Sysdig 详解

    Sysdig 是 Sysdig Cloud 开发的主要基于Lua语言的一个开源系统分析工具 Sysdig 能从运行的系统中 获取系统状态和行为 做过滤分析 功能上超同类开源工具 Sysdig 可以看做是 strace tcpdump lso
  • Java将图片转换成透明底图片

    package com stefan test import java awt Graphics2D import java awt Image import java awt image BufferedImage import java
  • android黑科技系列——手机端破解神器MT的内购VIP功能破解教程

    一 前言 在破解app的时候 我们现在几乎都是在PC端进行操作 但是之前bin神的MT管理器 可以在手机端直接破解 不过也有很大的局限性 但是对于一些简单的app破解没问题的 这个工具其实原理也很简单 就是解析apk中的dex arsc等文
  • 宝兰德BES中间件单机版安全加固相关操作

    因某公司对安全的要求 需要对bes中间件进行安全加固 安全加固的内容主要包括对控制台默认地址修改 实例默认端口修改 控制台默认密码修改 密码加密 加密后的实例启停方式等 首先说一下今天安全加固操作的前提 需要已经分离式部署安装成功bes单机
  • 什么是数据结构?什么是算法

    记得是大一大二的时候学习了数据结构 时间过的好快 现在实现了 现在感觉自己的基础好差很多都不会 欠的帐还是要还的 什么是数据结构 什么是算法 呃呃呃呃 哎 不会 多次参加了MOOC姥姥的数据结构 都没有坚持下来 希望这次可以坚持下来 引用姥

随机推荐

  • MySQL 查询表字段

    复制表结构 CREATE TABLE tb students copy LIKE tb students info 查询mysql所有表数据 字段信息mysql 查询表 所有字段 前2种有重复 select COLUMN NAME colu
  • LINQ之路 8: 解释查询(Interpreted Queries)

    LINQ提供了两个平行的架构 针对本地对象集合的本地查询 local queries 以及针对远程数据源的解释查询 Interpreted queries 在讨论LINQ to SQL等具体技术之前 我们有必要先对这两种架构进行了解和学习
  • Redis的五种数据类型详解

    目录 一 基础的知识 二 五种数据类型 1 String 字符串 2 List 列表 3 Set 集合 4 Hash 哈希表 5 Zset 有序集合 本篇文章更多的是通过命令来彻底掌握Redis的五种数据结构 一 基础的知识 Linux上安
  • 【Windows提权】内核提权

    文章目录 一 描述 二 缓冲区溢出 三 实验环境 四 提权 一 描述 提高自己在服务器中的权限 主要针对网站入侵过程中 当入侵某一网站时 通过各种漏洞提升WEBSHELL权限以夺得该服务器权限 提权 顾名思义就是提高自己在服务器中的权限 就
  • Vue常用的组件库大全【前端工程师必备】

    Vue常用的组件库大全 前端工程师必备 一 移动端 常用组件库 1 Vant ui 2 Cube UI 3 VUX 4 NuTUI 3 0 6 倒计时组件库 7 表格组件库 8 富文本编辑器 9 Loading加载动画组件 10 Mint
  • 【论文解读】元学习:MAML

    一 简介 元学习的目标是在各种学习任务上训练模型 这样它就可以只使用少量的训练样本来解决新任务 论文所提出的算法训练获取较优模型的参数 使其易于微调 从而实现快速自适应 该算法与任何用梯度下降训练的模型兼容 适用于各种学习问题 包括分类 回
  • semantic-ui的使用

    好久又没有写博客了 总是以忙为借口 哎 知识如果只图一时的学习之快 不加以总结输出 总是会忘记 希望自己可以好好规划一下吧 最近任务需要 学习了semantic ui这个前端框架 这里先基本记录一下如何去使用它 我在网上找到了一个seman
  • python-半省略号、三个点、点点点、...符号的用法小结

    一 符号 省略号 和Ellipsis对象 在Python中 一切皆对象 符号 在python中其实也有相应的对象 Ellipsis 如下面的代码所示 和Ellipsis的地址是相同的 打印 显示的结果是Ellipsis 打印 的类型显示el
  • 直流输入过压保护电路

    在嵌入式产品设计中 许多都为电池供电或者USB接口供电 当误操作使供电电压高于芯片工作电压时 就会导致芯片烧坏 带来严重的后果 因此在电源的输入端到芯片的供电输入端 还是很有必要加一个过压保护电路 当输入电压超过设定值 就切断电源 保护芯片
  • java登录远程服务器

    加入ssh的依赖 ch ethz ganymed ganymed ssh2 262 代码实现 因为执行SHELL脚本的时候可能是给出正常的返回值 也可能是异常的信息 这里需要一个JAVA Bean来接收这两个消息 代码如下 import j
  • OpenMMLab开源库总结——笔记1

    摘要 很高兴能加入OpenMMLab AI实战营 成为第二期4班的一名学员 OpenMMLab经过几年的发展和沉淀 其开源项目已经覆盖到计算机视觉的各个领域 OpenMMLab 为香港中文大学 商汤科技联合实验室 MMLab 开源的算法平台
  • mysql软件架构

    客户端是你的业务应用 怎么连接mysql server 一般都是用jdbc Java odbc C 客户端连接mysql 服务 发指令发sql语句等 文件系统 操作系统的文件系统 比如windows的文件系统 linux的文件系统 综上所述
  • 蓝桥杯模拟-排列序数

    问题 X星系的某次考古活动发现了史前智能痕迹 这是一些用来计数的符号 经过分析它的计数规律如下 为了表示方便 我们把这些奇怪的符号用a q代替 abcdefghijklmnopq 表示0 abcdefghijklmnoqp 表示1 abcd
  • 数据结构例题--迷宫

    迷宫 问题要求 定义一个二维数组N M 其中2 lt N lt 10 2 lt M lt 10 如5x5数组 如下所示 int maze 5 5 0 1 0 0 0 0 1 1 1 0 0 0 0 0 0 0 1 1 1 0 0 0 0 1
  • 记录红米k40解BL、Root、装XPOSED

    一 前言 刚从一加转到红米 不得不吐槽虽然一加系统简陋但社区搞基搞机氛围非常好 官方支持root也保修和第三方ROM下大神层出不群 小米的社区放眼望去基本没人讨论root 第三方ROM 都是各种吐槽提问但没人回复的帖子 既然找不到教程那就按
  • qt,信号槽连接不成功原因

    在 Qt 中 信号槽连接不成功的原因可能有很多 下面是一些可能的原因和解决方法 1 信号和槽的声明不匹配 请确保信号和槽的参数类型和数量完全匹配 如果它们不匹配 连接将不会成功 检查信号和槽的声明 确保它们是兼容的 2 信号和槽的作用域问题
  • JSON数组,JSON对象,数组的区别与基本操作整理

    JSON 语法规则 JSON 语法是 javascript 对象表示语法的子集 数据在名称 值对中 数据由逗号分隔 花括号保存对象 方括号保存数组 JSON 名称 值对 JSON 数据的书写格式是 名称 值对 名称 值对组合中的名称写在前面
  • 三大WEB服务器对比分析(apache ,lighttpd,nginx)

    一 软件介绍 apache lighttpd nginx 1 lighttpd Lighttpd是一个具有非常低的内存开销 cpu占用率低 效能好 以及丰富的模块等特点 lighttpd是众多OpenSource轻量级的web server
  • 微信小程序开发(七) swiper 组件

    微信小程序swiper组件 banner 图 广告页 splash 实现效果 代码 js Page data iamgeUrls https img zcool cn community 0106445dc28607a801209e1f62
  • C++11右值 &&引用

    C 11右值 引用 2017年06月06日 11 56 25 Ink cherry 阅读数 2394 标签 C 右值引用 完美转发 个人分类 C C 感觉这篇讲的比较深入细致了 存一下 via http www cnblogs com Ti