200字范文,内容丰富有趣,生活中的好帮手!
200字范文 > 左值 右值 右值引用 移动 引用坍缩和完美转发

左值 右值 右值引用 移动 引用坍缩和完美转发

时间:2022-05-01 10:56:50

相关推荐

左值 右值 右值引用 移动 引用坍缩和完美转发

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,广义左值一个xvalueexpiring lvalue,将亡值一个prvaluepure 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_;};

从实际内存布局的角度,很多语言——如 Java 和 Python——会在 A 对象里放 B 和 C 的指针(虽然这些语言里本身没有指针的概念)。而 C++ 则会直接把 B 和 C 对象放在 A 的内存空间里。这种行为既是优点也是缺点。说它是优点,是因为它保证了内存访问的局域性,而局域性在现代处理器架构上是绝对具有性能优势的。说它是缺点,是因为复制对象的开销大大增加:在 Java 类语言里复制的是指针,在 C++ 里是完整的对象。这就是为什么 C++ 需要移动语义这一优化,而 Java 类语言里则根本不需要这个概念。

移动语义使得在 C++ 里返回大对象(如容器)的函数和运算符成为现实,因而可以提高代码的简洁性和可读性,提高程序员的生产率。所有的现代 C++ 的标准容器都针对移动进行了优化。

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)。

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。