Reference
此篇基本是上述内容备份。
标准里关于值的分类:
一个lvalue
是通常可以放在等号左边的表达式,左值(左值 lvalue是有标识符、可以取地址的表达式.在函数调用时,左值可以绑定到左值引用的参数,如T&
。一个常量只能绑定到常左值引用,如const T&
。
变量、函数或数据成员的名字
返回左值引用的表达式,如 ++x、x = 1、cout << ’ '(x = 1 和 ++x 返回的都是对 x 的 int&。x++ 则返回的是 int)
cout << "cout << ' ' = " << &(cout << ' ') << endl;// 运行结果:// cout << ' ' = 0x6fd0acc0
字符串字面量如 “hello world”(不是临时的,可以取地址,&(“hello world”))
一个rvalue
是通常只能放在等号右边的表达式,右值一个glvalue
是 generalized lvalue,广义左值一个xvalue
是expiring lvalue,将亡值一个prvalue
是pure rvalue,纯右值纯右值prvalue
是没有标识符、不可以取地址的表达式,一般也称之为“临时对象” 返回非引用类型的表达式,如 x++、x + 1、make_shared(42)除字符串字面量之外的字面量,如 42、true(临时的,不能取地址
在
smart_ptr&& other
里面的other是左值——虽然它的类型是右值引用。拿这个 other 去调用函数时,它匹配的也会是左值引用。也就是说,类型是右值引用的变量是一个左值!但对于一个右值引用的变量,你是可以取地址的,这点上它和左值完全一致。
C++11 之前
,右值可以绑定到常左值引用的参数,如const T&
,但不可以绑定到非常左值引用,如T&
。(右值不能取地址,意味着不能修改).C++11 开始
,C++
语言里多了一种引用类型——右值引用。右值引用的形式是T&&
.移动语义是 C++11 里引入的一个重要概念。在使用容器类的情况下,移动更有意义。
string result =string("Hello, ") + name + ".";
C++11
之前:
调用构造函数 string(const char*),生成临时对象 1;"Hello, " 复制 1 次。调用operator+(const string&, const string&),生成临时对象 2;"Hello, " 复制 2 次,name 复制 1 次。调用 operator+(const string&, const char*),生成对象 3;“Hello, " 复制 3 次,name 复制 2 次,”." 复制 1 次。假设返回值优化能够生效(最佳情况),对象 3 可以直接在 result 里构造完成。临时对象 2 析构,释放指向 string("Hello, ") + name 的内存。临时对象 1 析构,释放指向 string("Hello, ") 的内存。
可以改写啰嗦的写法以避免构造临时对象:
string result = "Hello, ";result += name;result += ".";
C++11
开始后,间接写法的流程:
调用构造函数 string(const char*),生成临时对象 1;"Hello, " 复制 1 次。调用operator+(string&&, const string&),直接在临时对象1身上追加操作,并将结果移动到临时对象2;name 复制 1 次。调用 operator+(string&&, const char*),直接在临时对象 2 上面执行追加操作,并把结果移动到 result;"." 复制 1 次。临时对象2析构,内容已经为空,不需要释放内存。临时对象1析构,内容已经为空,不需要释放内存。
性能上,所有的字符串只复制了一次;虽然比啰嗦的写法仍然要增加临时对象的构造和析构,但由于**这些操作不牵涉到额外的内存分配和释放,是相当廉价的。**程序员只需要牺牲一点点性能,就可以大大增加代码的可读性。
此外很关键的一点是,C++ 里的对象缺省都是值语义。在下面这样的代码里:
class A {B b_;C c_;};
移动语义使得在 C++ 里返回大对象(如容器)的函数和运算符成为现实,因而可以提高代码的简洁性和可读性,提高程序员的生产率。所有的现代 C++ 的标准容器都针对移动进行了优化。从实际内存布局的角度,很多语言——如 Java 和 Python——会在 A 对象里放 B 和 C 的指针(虽然这些语言里本身没有指针的概念)。而 C++ 则会直接把 B 和 C 对象放在 A 的内存空间里。这种行为既是优点也是缺点。说它是优点,是因为它保证了内存访问的局域性,而局域性在现代处理器架构上是绝对具有性能优势的。说它是缺点,是因为复制对象的开销大大增加:在 Java 类语言里复制的是指针,在 C++ 里是完整的对象。这就是为什么 C++ 需要移动语义这一优化,而 Java 类语言里则根本不需要这个概念。
std::move() , std::forward()
引用坍缩和完美转发
对于一个实际的类型 T,它的左值引用是 T&,右值引用是 T&&。
是不是看到 T&,就一定是个左值引用?
是不是看到 T&&,就一定是个右值引用?
对于前者的回答是“是”,对于后者的回答为“否”。
关键在于,在有模板的代码里,对于类型参数的推导结果可能是引用。(引用坍缩(折叠)发生在“模板实例化之类的上下文”中,或auto
定义变量的过程中,二者的参数推导类似,第三个引用坍缩场景是typedef
定义的形成和使用,第四个是decltype expressions
,但规则略不同):
对于template<typename> foo(T&&)
这样的代码:
如果传递过去的参数是左值,T 的推导结果是左值引用; 如果T 是左值引用,那 T&& 的结果仍然是左值引用——即type& && 坍缩成了 type&。(因为C++中不允许引用的引用)。 如果传递过去的参数是右值,T 的推导结果是参数的类型本身。 如果T 是一个实际类型,那T&& 的结果自然就是一个右值引用。
void foo(const shape&){puts("foo(const shape&)");}void foo(shape&&){puts("foo(shape&&)");}void bar(const shape& s){puts("bar(const shape&)");foo(s);}void bar(shape&& s){puts("bar(shape&&)");foo(s);}int main(){bar(circle());}
输出:
bar(shape&&)
foo(const shape&)
如果我们要让 bar 调用右值引用的那个 foo 的重载:则
foo(std::move(s));
或
foo(static_cast<shape&&>(s));// C++11为static_cast添加的功能,显式地将一个左值转换为一个右值。
事实上,很多标准库里的函数,连目标的参数类型都不知道,但我们仍然需要能够保持参数的值类别:左值的仍然是左值,右值的仍然是右值。这个功能在 C++ 标准库中已经提供了,叫std::forward
。它和 std::move 一样都是利用引用坍缩机制来实现。
可以通过std::forward()将bar()合并为一个
template <typename T>void bar(T&& s) {foo(std::forward<T>(s));}
circle temp;bar(temp);bar(circle());
输出:
foo(const shape&)
foo(shape&&)
因为在 T 是模板参数时,T&& 的作用主要是保持值类别进行转发,它有个名字就叫“转发引用”(forwarding reference)。因为既可以是左值引用,也可以是右值引用,它也曾经被叫做“万能引用”(universal reference)。