constructor
2020-06-25 00:14:10
先思考如下几个问题:
int& fun(void){...}
, 请问fun()
这个表达式的类型是什么对一下答案吧:
int&
如果你的作答和上面的答案一样,那么恭喜你。
全错
来吧,一起看下面值类别的内容,然后再回过头来考虑上面的问题。
在C++11以前,值类别被分为左值(lvalue)和右值(rvalue),11之后,右值被进一步细化,分为亡值(xvalue, expiring value)和纯右值(prvalue, pure right value)。(注意,bitfield只能归类于泛左值(glvalue, general left value),因为无法确定其是左值还是亡值,但是本文章不会引入位域,请忽略)
这边要引入一个典型的误区,认为能放在赋值运算符(assignment operator)的左侧的表达式为左值,其余的为右值(note: 一个更错误的想法是放左边的是左值,放右边的是右值)。
正常人都会见到这个名字后作出如上的猜测,事实上,在历史上,这个说法一度是正确的,编程语言CPL第一次引入了值类别的概念,并且规定能放置于赋值运算符左侧的表达式为左值表达式,否则为右值表达式,但这样的规则在C++显然不成立,反例有如下几种:
const int a = 0; // a这个id-expr是左值表达式,但a不能被放在赋值运算符左侧
"Luogu" // 字符串常量是左值表达式,但是常量表达式显然不能放在运算符左侧
int arr[3];
arr // arr作为id-expr时是左值表达式,但是不能被放在赋值运算符左侧
每个C++表达式都由两部分构成,一个是类型,一个是值类别。而且,这个类型绝对不可能是引用类型(http://eel.is/c++draft/expr.type#1 ),因为根本不存在引用类型的对象(因为引用类型不是对象类型,http://eel.is/c++draft/basic.types#8 )。所谓的引用类型的对象这种说法都是错误的,也正是基于这个理由,才需要std::reference_wrapper
这种设施。
在C++98中,并没有左值和右值的明确定义,只能按照性质予以区分。
最不会出错的分辨方法是左值可以被应用于取地址运算符(&, address-of operator)
[cling]$ const int a = 0;
[cling]$ &a
(const int *) 0x7f24112dc000
[cling]$ &"Luogu"
(const char (*)[6]) 0x7f241129a000
[cling]$ int arr[10]{};
[cling]$ &arr
(int (*)[10]) 0x7f24112de010
[cling]$ &2
input_line_9:2:2: error: cannot take the address of an rvalue of type 'int'
&2
^~
事实上,以下的几种是左值表达式:
注意右值引用的变量名表达式当然也是左值,尽管它并不是一个对象
[cling]$ int&& r = 5
(int) 5
[cling]$ &r
(int *) 0x7f3d20ea0010
见引言问题1,正确答案应该是int类型的左值。
赋值表达式和组合赋值表达式(+=
, %=
, etc. compound assignment expression)
内建的间接访问表达式(*
, indirection expression, a.k.a. dereference expression, 解引用表达式)
内建的下标表达式([]
, subscript expression),请注意,内建的下标表达式只是对间接访问运算符的替换,请尝试2["Luogu"]
内建前置自增表达式(pre-increment and pre-decrement expression)
成员访问运算符(.
, the member of object expression,但是这个运算符一般叫做member access operator)
字符串字面量
这不是全部,部分本文读者较为不熟悉的内容以及一些过于简单的内容略去
可以被取地址
可以转换到纯右值
可以拥有不完全类型
比如,extern数组前向声明:
extern int a[];
可以作为赋值的目标对象
可以被非到const的左值引用绑定
可以注意到,CPL时期的左值指的都是可修改的左值
返回值非引用类型的函数调用表达式
后自增减表达式(post-increment and post-decrement expression)
内建的算数表达式、逻辑表达式和比较表达式(arithmetic expression, logical expression, and comparison expression)
取地址表达式
this
除了字符串字面量外的字面量
一个比较有意思的一点是,自定义字面量也都是纯右值。
//-std=c++14
[cling]$ #include <string>
[cling]$ using namespace std::literals;
[cling]$ & "ss"
(const char (*)[3]) 0x7fc4ffb8e000
[cling]$ &"ss"s
input_line_6:2:2: error: taking the address of a temporary object of type 'basic_string<char>'
[-Waddress-of-temporary]
&"ss"s
^~~~~~
枚举量(enumerator),我觉得这也算字面量的一种
C++11起的lambda expression
这不是全部,部分本文读者较为不熟悉的内容以及一些过于简单的内容略去
const std::string
和std::string
的不同)int fun()
const int fun() // the same
std::string foo()
const std::string foo() // not the same
亡值从C++11开始引入,是为了完善移动语义(move semantics)
最典型的应该就是std::move
, 请注意std:::forward
不一定是,请不要混淆forwarding reference和rvalue reference,限于篇幅,请自己查找有关资料。std::move
并不是一个很玄学的东西,只是一个强制转换而已。移动语义的功劳经常被抢,很多人以为移动是std::move
做的,其实这个函数和移动没有半点关系,只是改变了值类别让一个对象可以进行移动。
std::move
里面干的就是这个)这不是全部,部分本文读者较为不熟悉的内容以及一些过于简单的内容略去
除以下几点外,具有左值和纯右值的性质:
数组名是指向首地址的指针这种谬论大概是从谭浩强教授开始的。本来这句话听着就不对劲,但是被人传久了好像大家都认为这句话没什么问题了。
看几个例子:
[cling]$ int a[50];
[cling]$ sizeof a == sizeof(int*)
(bool) false
int a[50];
int (&b) [50] = a;
很好的证明了数组名并不是指针,而就指代数组本身。
然而,之所以存在这一误区,就是因为C++的退化机制(decay)。在不需要数组的任何语境,数组名隐式转换为指向首元素的纯右值指针表达式。函数也拥有同样的退化性质,但是函数名也好,数组名也罢,他们都是左值。
C++11引入了移动语义,目的是节省复制的开销。
//-std=c++11
std::vector<int> source = {2, 2, 1, 2, 3};
//after this, vector destination takes the memory managed by source, accesses to source become Undefined Behaviors.
auto destination = std::move(source);
为了解释这段代码,我将std::move
去掉,写成这样:
auto d = static_cast<decltype(s) &&>(s)
左值到右值引用强制转换表达式是亡值,这使得本来不可被移动的左值s可以被移动,由于转换后的亡值与原有的左值是同一实体,他移交的内存也是原有对象管理的内存。这个亡值参与的初始化语句是移动语境:重载决议将选定vector<int>(vector<int>&&)
这个移动构造函数。典型的vector移动实现是直接移交源对象的指针给目标对象,这避免了元素的复制开销。
相比而言,如果不进行一次强制转换,选定的构造函数将会是vector<int>(const vector<int>&)
这个移动构造函数,从上面可以发现,当参数是右值时,函数右值引用的版本在重载决议中高于到const引用的版本,尽管后者也是可以绑定到右值的,另外,C++17中纯右值保证不调用构造函数,连移动的开销一起省了(甚至移动复制构造函数全=delete
也能过编译,这件事经常迷惑一批萌新),所以注意构造和析构函数中的副作用(side-effect)是不能被依赖的。
所以如果并没有给定移动构造函数,上述的代码不会报错,而是会调用复制构造,但不影响C++17开始的强制复制消除。
说道移动语义就一定要安利C++14的std::exchange
,简直是算法题目中能清理大量代码的福音:https://en.cppreference.com/w/cpp/utility/exchange
之前已经提到了值类别的来源,CPL。在这之后,C基本沿袭了CPL的术语,但是废除了“右值”这一名词,将值类别分为左值和非左值,C++98恢复了右值这一术语,并将C中的一些非左值变为左值。
C++11开始,随着移动语义的引入,右值被分开,并出现了较为严格的值类别定义:
其中,同一性指的是有办法判断两个表达式指代的是否是同一实体。
至于移动语义请自己去查找。
C++17开始,C++11的分类法被完全推翻。
C++17开始纯右值不可被移动,并且引入了强制复制消除的要求(mandatory copy elision),达到了史无前例的值类别最复杂的阶段,另外,void表达式开始指代一个无结果的对象,也成为了不可被移动且不具有同一性的纯右值。
本文章是参考资料加上个人见解写成,其中不免可能出现错误,欢迎大家指出。
参考:http://eel.is/c++draft/basic.lval