右值引用就是对一个右值进行引用的类型。因为右值不具名,所以我们只能通过引用的方式找到它。
无论声明左值引用还是右值引用都必须立即进行初始化,因为引用类型本身并不拥有所绑定对象的内存,只是该对象的一个别名。通过右值引用的声明,该右值又“重获新生”,其生命周期与右值引用类型变量的生命周期一样,只要该变量还活着,该右值临时量将会一直存活下去。
看一下下面的代码:
#include using namespace std; int g_constructCount=0; int g_copyConstructCount=0; int g_destructCount=0; struct A { A(){ cout<<"construct: "<<++g_constructCount<
从上面的例子中可以看到,在没有返回值优化的情况下,拷贝构造函数调用了两次,一次是GetA()函数内部创建的对象返回后构造一个临时对象产生的,另一次是在main函数中构造a对象产生的。第二次的destruct是因为临时对象在构造a对象之后就销毁了。如果开启返回值优化,输出结果将是:
construct: 1
destruct: 1
可以看到返回值优化将会把临时对象优化掉,但这不是C++标准,是各编译器的优化规则。我们在回到之前提到的可以通过右值引用来延长临时右值的生命周期,如果在上面的代码中我们通过右值引用来绑定函数返回值,结果又会是什么样的呢?在编译时设置编译选项
-fno-elide-constructors。int main() { A&& a = GetA(); return 0; } 输出结果: construct: 1 copy construct: 1 destruct: 1 destruct: 2
通过右值引用,比之前少了一次拷贝构造和一次析构,原因在于右值引用绑定了右值,让临时右值的生命周期延长了。我们可以利用这个特点做一些性能优化,即避免临时对象的拷贝构造和析构,事实上,在C++98/03中,通过常量左值引用也经常用来做性能优化。将上面的代码改成:const A& a = GetA();输出的结果和右值引用一样,因为常量左值引用是一个“万能”的引用类型,可以接受左值、右值、常量左值和常量右值。需要注意的是普通的左值引用不能接受右值,比如这样的写法是不对的:A& a = GetA();上面的代码会报一个编译错误,因为非常量左值引用只能接受左值。实际上T&&并不是一定表示右值,它绑定的类型是未定的,既可能是左值又可能是右值。看看这个例子:
template void f(T&& param); f(10); // 10是右值 int x = 10; f(x); // x是左值
从这个例子可以看出,param有时是左值,有时是右值,因为在上面的例子中有&&,这表示param实际上是一个未定的引用类型。这个未定的引用类型称为universal references(可以认为它是一种未定的引用类型),它必须被初始化,它是左值还是右值引用取决于它的初始化,如果&&被一个左值初始化,它就是一个左值;如果它被一个右值初始化,它就是一个右值。需要注意的是,只有当发生自动类型推断时(如函数模板的类型自动推导,或auto关键字),&&才是一个universal references。 template
void f(T&& param); // 这里T的类型需要推导,所以&&是一个universal references template class Test { ... Test(Test&& rhs); // 已经定义了一个特定的类型, 没有类型推断 ... // && 是一个右值引用 }; void f(Test&& param); //
已经定义了一个确定的类型, 没有类型推断,&& 是一个右值引用
再看一个复杂一点的例子:
template
void f(std::vector&& param);
这里既有推断类型T又有确定类型vector,那么这个param到底是什么类型呢?答案是它是右值引用类型,因为在调用这个函数之前,这个vector中的推断类型已经确定了,所以到调用f时没有类型推断了。再看下面这个例子:templatevoid f(const T&& param);这个param是universal references吗?其实它也是一个右值引用类型。读者也许会不明白,T不是推断类型吗,怎么会是右值引用类型?其实还有一条很关键的规则:universal references仅仅在T&&下发生,任何一点附加条件都会使之失效,而变成一个普通的右值引用。因此,上面的T&&在被const修饰之后就成为右值引用了。由于存在T&&这种未定的引用类型,当它作为参数时,有可能被一个左值引用或者右值引用的参数初始化,这时经过类型推导的T&&类型,相比右值引用(&&)会发生类型的变化,这种变化被称为引用折叠。C++11中的引用折叠规则如下:1)所有的右值引用叠加到右值引用上仍然还是一个右值引用。2)所有的其他引用类型之间的叠加都将变成左值引用。左值或者右值是独立于它的类型的,右值引用可能是左值也可能是右值。比如下面的例子:int&& var1 = x; // var1 的类型是 int&&// var2存在类型推导,因此是一个universal references。这里auto&&最终会被推导为int&auto&& var2 = var1;其中,var1的类型是一个左值类型,但var1本身是一个左值;var1是一个左值,根据引用折叠规则,var2是一个int&。下面再来看一个例子:int w1, w2;auto&& v1 = w1;decltype(w1)&& v2 = w2;其中,v1是一个universal reference,它被一个左值初始化,所以它最终是一个左值;v2是一个右值引用类型,但它被一个左值初始化,一个左值初始化一个右值引用类型是不合法的,所以会编译报错。但是,如果希望把一个左值赋给一个右值引用类型该怎么做呢?用std::move: decltype(w1)&& v2 = std::move(w2);
std::move可以将一个左值转换成右值。关于std::move的内容将在下一节介绍。
编译器会将已命名的右值引用视为左值,而将未命名的右值引用视为右值。看下面的例子:
void PrintValue(int& i) { std::cout<<"lvalue : "< #include #ifndef _MSC_VER #include #endif #include #include #include template std::string type_name() { typedef typename std::remove_reference::type TR; std::unique_ptr own ( #ifndef __GNUC__ nullptr, #else abi::__cxa_demangle(typeid(TR).name(), nullptr, nullptr, nullptr), #endif std::free ); std::string r = own != nullptr ? own.get() : typeid(TR).name(); if (std::is_const::value) r += " const"; if (std::is_volatile::value) r += " volatile"; if (std::is_lvalue_reference::value) r += "&"; else if (std::is_rvalue_reference::value) r += "&&"; return r; } template void Func(T&& t) {
cout<()<在上述例子中,abi::__cxa_demangle将低级符号名解码(demangle)成用户级名字,使得C++类型名具有可读性。可以通过一个例子展示abi::__cxa_demangle的作用,代码如下:
#include #include class Foo {}; int main() { class Foo {}; std::cout << typeid(Foo*[10]).name() << std::endl; class Foo {}; std::cout << typeid(Foo*[10]).name() << std::endl; } 结果如下: class Foo * [10] // vc A10_P3Foo // gcc 在gcc中,类型名很难看明白,为了输出让用户能看明白的类型,在gcc中需作如下修改: #include #include #ifndef _MSC_VER #include #endif class Foo {}; int main() { char* name = abi::__cxa_demangle(typeid(Foo*[10]).name(), nullptr, nullptr, nullptr); std::cout << name << std::endl; free(name); return 0; } 结果如下: Foo* [10]
这个例子中的std::unique_ptr是智能指针,相关内容将在第4章中介绍。可以看到,当T&&为模板参数时,如果输入左值,它会变成左值引用,而输入右值时则变为无引用的类型。这是因为T&&作为模板参数时,如果被左值X初始化,则T的类型为X&;如果被右值X初始化,则T的类型为X。&&的总结如下:1)左值和右值是独立于它们的类型的,右值引用类型可能是左值也可能是右值。2)auto&&或函数参数类型自动推导的T&&是一个未定的引用类型,被称为universal references,它可能是左值引用也可能是右值引用类型,取决于初始化的值类型。3)所有的右值引用叠加到右值引用上仍然是一个右值引用,其他引用折叠都为左值引用。当T&&为模板参数时,输入左值,它会变成左值引用,而输入右值时则变为具名的右值引用。4)编译器会将已命名的右值引用视为左值,而将未命名的右值引用视为右值。