Reading Note of CppPrimer-Chapter13
Copy Control
Copy & Assign & Destroy
拷贝控制 (copy control)
拷贝和移动构造函数定义了当用同类型的另一个对象初始化本对象时做什么;拷贝和移动赋值函数定义了将一个对象赋予另一个对象的时候做什么;虚构函数定义了当此类型对象销毁的时候做什么;我们称这些操作为拷贝控制操作
Copy Constructor
拷贝构造函数 (copy constructor)
第一个参数是自身类型的
const reference
作为参数,如果有其他参数,那么其他参数都有默认值;拷贝构造函数依然可以使用成员初始化列表;
1
2
3
4
5
6
7
8
9
10class Foo{
Foo() = default;
Foo(const Foo &rhs);
};
class Bar{
Bar() = default;
Bar(const Foo &rhs, int type=1): val(rhs.val){} // member initializer list
private:
int val = 0;
};为什么类的拷贝构造函数的参数一定需要是
const reference
的为什么一定是要
reference
如果拷贝构造函数的参数不是引用,则调用陷入死循环:调用拷贝构造函数,必须拷贝他的实参;拷贝实参,又必须调用拷贝构造函数,无限循环;
为什么一定是
const reference
拷贝的时候一般并不涉及到对被拷贝对象的更改
Logically, it should make no sense to modify an object of which you just want to make a copy, though sometimes it may have some sense, like a situation where you’d like to store the number of time this object has been copied. But this could work with a
mutable
member variable that stores this information, and can be modified even for a const object (and the second point will justify this approach)如果形参是非 const 的,但是实参是 const 的,则参数无法匹配,无法调用拷贝构造函数
You would like to be able to create copy of const objects. But if you’re not passing your argument with a const qualifier, then you can’t create copies of const objects…
临时变量只能绑定到 const reference 上面
You couldn’t create copies from temporary reference, because temporary objects are rvalue, and can’t be bound to reference to non-const. For a more detailed explanation, I suggest Herb Sutter’s article on the matter
拷贝构造函数的执行
拷贝构造函数的一般作用是将参数的各个数据成员拷贝到该类类型的对象中;类成员的类型决定了采取何种方式拷贝:
- 如果是类类型,使用其拷贝构造函数拷贝;
- 如果是内置类型,直接拷贝;
- 对于数组,其元素的拷贝方式和类成员的拷贝方式相同;
拷贝初始化 (copy initialization)
拷贝初始化要求编译器将右侧运算对象拷贝到正在创建的对象中,如果需要的话,还要做类型转换;
拷贝初始化通常使用拷贝构造函数,但是如果一个类有一个移动构造函数,则拷贝初始化有时候会使用移动构造函数
拷贝初始化在几种情况下都会被隐式调用,所以大多数情况下不应该是
explicit
的;拷贝初始化出现在:- 用
=
来定义一个新的变量的时候 (如果不是新变量就是调用拷贝赋值函数了); - 将一个对象作为实参传递给一个非引用类型的形参(传值拷贝);
- 从一个返回类型为非引用类型的函数返回一个对象(作为返回值);
- 用花括号列表初始化一个数组中的元素或者聚合类中的成员;
1
2
3
4
5std::string dots(10, '9'); // 直接初始化
std::string s(dots); // 直接初始化
std::string s2 = dots; // 拷贝初始化
std::string s3 = "9-9999-9999-9"; // 拷贝初始化,类型转换
std::string s4 = std::string(100, '0'); // 拷贝初始化1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33class Test {
public:
Test(){std::cout << "construct" << std::endl;}
Test(const Test &a){std::cout << "copy construct" << std::endl;}
};
// case 2: function non reference argument
void set_test_obj(Test a){
}
// case 3: function non reference return value
Test get_test_obj(){
Test a;
return a; // clang with option -fno-elide-constructors to elimate return value optimization
}
// case 4: aggregate class initialize-list init
struct TestStruct{
Test a;
Test b;
};
std::cout << "\ncase1: ----------" << std::endl;
Test a;
Test b = a;
std::cout << "\ncase2: ----------" << std::endl;
set_test_obj(a);
std::cout << "\ncase3: ----------" << std::endl;
Test c = get_test_obj();
std::cout << "\ncase4.1: ----------" << std::endl;
Test array[5] = {a, b, c};
std::cout << "case4.2: ----------" << std::endl;
TestStruct t_struct = {a, b};1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23clang++ i.cpp -o test -std=c++17 -fno-elide-constructors
./test
case1: ----------
construct
copy construct
case2: ----------
copy construct
case3: ----------
construct
copy construct
case4.1: ----------
copy construct
copy construct
copy construct
construct
construct
case4.2: ----------
copy construct
copy construct- 用
编译器在编译拷贝构造函数的时候,可以绕过拷贝 / 移动构造函数,直接创建对象。但是即使略过了他们,在这个程序点拷贝 / 移动构造函数必须是存在的,比如不能是
private
或deleted
;比如编译器允许将以下代码改写:1
2
3
4
5// change
std::string null_book = "9999-9999-9999"; // copy construct
// to
std::string null_book("9999-9999-9999"); // construct directly
Copy Assignment Operator
拷贝赋值运算符
本质上是一个拷贝赋值函数 (
operator=
),注意区分拷贝赋值函数和拷贝构造函数;赋值运算符必须定义为类的成员函数如果没有手工定义拷贝赋值函数,编译器会生成一个合成拷贝赋值函数 (synthesized copy-assignment operator)
合成拷贝赋值函数会将右侧对象的每个 non-static 数据成员赋予左侧对象的对应数据成员;如果数据成员为数组,则会逐个赋值数组的元素
拷贝赋值运算符(函数)一般接受一个与所在类相同类型的
const reference
参数,为什么是const reference
的原因和拷贝构造函数的原因类似;1
Sales_data& operator=(const Sales_data& rhs);
当然拷贝构造函数也可以接受传值调用,传值调用和 swap 搭配可以很好解决自赋值的问题,且天然是异常安全的
1
2
3
4Sales_data& operator=(Sales_data rhs){
std::swap(*this, rhs); // exception safe
return *this;
}拷贝赋值函数应该返回一个指向其左侧运算对象的引用 ;
1
2
3Sales_data& operator=(const Sales_data& rhs){
return *this;
}
Destructor
析构函数负责释放对象使用的资源,并销毁类的 non-static 数据成员;析构函数没有参数,所以不可以被重载;一个类只可以有唯一的析构函数
析构函数的执行方向和构造函数相反
在执行构造函数的函数体之前,类的成员变量就已经初始化完成了
相应的,类的成员变量并不是在析构函数函数体内析构的,而是在函数体之后隐含的析构阶段中被销毁的
- 先执行析构函数的函数体,然后按照类成员声明的反方向销毁类成员;
- 派生类的析构函数先于基类的析构函数
无论何时一个对象被销毁,都会自动调用析构函数:
- 变量离开作用域(引用和指针除外);
- 对象被销毁,其成员也被销毁;
- 容器被销毁,容器的元素也被销毁;
- 动态分配内存的对象,delete 的时候被销毁;
- 临时对象在创建完他的表达式执行完毕后就被销毁了
如果没有定义自己的虚构函数,同样的编译器会合成虚构函数;合成虚构函数的函数体为空;
Rules
rule of zero
Classes that don’t manage resources should be designed so that the compiler-generated functions for copying, moving, and destruction do the right things.
rule of three
If a class requires a user-defined destructor, a user-defined copy constructor, or a user-defined copy assignment operator, it almost certainly requires all three.
Because C++ copies and copy-assigns objects of user-defined types in various situations (passing/returning by value, manipulating a container, etc), these special member functions will be called, if accessible, and if they are not user-defined, they are implicitly-defined by the compiler.
The implicitly-defined special member functions are typically incorrect if the class manages a resource whose handle is an object of non-class type (raw pointer, POSIX file descriptor, etc), whose destructor does nothing and copy constructor/assignment operator performs a “shallow copy“ (copy the value of the handle, without duplicating the underlying resource).
rule of five
Because the presence of a user-defined destructor, copy-constructor, or copy-assignment operator prevents implicit definition of the move constructor and the move assignment operator, any class for which move semantics are desirable, has to declare all five special member functions
Default / Delete
我们可以通过使用
=default
来显式要求编译器生成合成版本的拷贝/移动构造
、拷贝/移动赋值
、析构
、构造
函数等;我们可以通过使用
=delete
来指定编译器的拷贝/移动构造
、拷贝/移动赋值
、析构
、构造
函数等为删除的,不存在的;default
和delete
使用上的区别:=delete
只能出现在函数第一次声明的地方;而=default
只影响为这个成员而生成的代码,直到编译器生成代码的时候才需要- 任何函数都可以指定为
delete
的,只有具有合成版本的成员函数可以使用default
合成的拷贝控制成员可能是
delete
的- 如果一个类有数据成员不能默认
构造
、拷贝
、复制
或者销毁
,那么这个类本身的默认构造函数
、默认拷贝构造/赋值函数
、默认析构函数
等会被标记为delete
; - 如果一个类具有引用成员或者 const 成员,则他不能合成默认拷贝赋值运算符
- 如果一个类有数据成员不能默认
对于析构函数为
delete
的类型- 不能定义该类型的变量;
- 虽然可以 new 动态分配这种类型的变量但是不能释放指向该类型动态分配的指针,其占用的内存不能正常释放;
1
2
3
4
5
6
7
8struct NoDtor{
NoDtor() = default;
~NoDtor() = delete;
};
NoDtor nd; // error: attempt to use a deleted function
NoDtor *ndptr = new NoDtor();
delete ndptr; // error: attempt to use a deleted function阻止拷贝
阻止拷贝应该使用
deleted
来定义他们自己的拷贝构造函数
和拷贝控制运算符
,不应该将他们声明为private
的;新标准发布之前,类是通过将其拷贝构造函数和拷贝赋值运算符声明为
private
的来阻止拷贝,但是这样阻止不了成员函数和友元类拷贝;
Copy Control and Resource Management
一般按照不同的拷贝语义可以将类的行为看起来像是一个值或者一个指针
- 类的行为像一个值,意味着拷贝这个类的对象时,类的原对象和其副本是完全独立的,改变副本不会对原值有影响
- 类的行为像一个指针,意味着拷贝这个类的对象时,副本和原对象共享相同的底层数据,改变副本也会改变原值
行为像值的类的
拷贝赋值函数
,通常组合了析构函数
和拷贝构造函数
,通过先拷贝右侧运算对象,再释放左侧运算对象,可以正确处理自赋值的情况,也可以保证在异常发生的时候也是安全的,将自己置于一个有意义的状态;在编写赋值运算符的时候,记得:
- 如果一个对象将对象赋予他自己,赋值运算符必须可以正常工作
- 大多数
赋值运算符
集合了拷贝构造
和析构函数
,一个好的模式是先将拷贝赋值的参数拷贝到一个临时对象中;如果这个拷贝成功,那么销毁自己本身也是安全的,剩下的就是把临时对象的值拷贝给自己了;
1
2
3
4
5
6
7
8
9
10HasPtr& HasPtr::operator=(const HasPtr &rhs){
// call ctor
auto newp = new string(*rhs.ps);
// call dtor
delete ps;
// call copy-assignment
ps = newp;
i = rhs.i;
return *this;
}更简单的解决自赋值的问题,可以考虑拷贝构造函数的
传值调用+swap
1
2
3
4HasPtr& operator=(HasPtr rhs){
std::swap(*this, rhs); // exception safe
return *this;
}使一个类表现像指针的最好的方法是使用智能指针
shared_ptr
来管理类中的资源;当我们希望直接管理资源的时候,可以使用模拟
shared_ptr
的引用计数机制;引用计数的工作方式:- 所有的构造函数(除了拷贝构造函数)都需要初始化计数器
use_count
;初始化时只有一个对象共享状态,此时use_count=1
- 拷贝构造函数不分配新的计数器,拷贝
user_count
和其他成员的指针,递增共享的计数器use_count
- 析构函数递减共享的计数器
use_count
,如果use_count==0
,则调用析构函数释放对象 - 拷贝赋值运算符,递增右侧运算符的
use_count
,递减左侧运算符的use_count
;如果左侧运算符use_count==0
,则销毁对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36class HasPtr {
public:
// constructor allocates a new string and a new counter, which it sets to 1
HasPtr(const std::string &s = std::string()):
ps(new std::string(s)), i(0), use(new std::size_t(1)){}
// copy constructor copies all three data members and increments the counter
HasPtr(const HasPtr &p):
ps(p.ps), i(p.i), use(p.use) { ++*use; }
// copy assignment operator
HasPtr& operator=(const HasPtr&){
++*rhs.use; // increment the use count of the right-hand operand
if (--*use == 0) { // if no other users
delete ps; // then decrement this object's counter
delete use; // free this object's allocated members
}
ps = rhs.ps; // copy data from rhs into this object
i = rhs.i;
use = rhs.use;
return *this; // return this object
}
// destructor
~HasPtr(){
if (--*use == 0) { // if the reference count goes to 0
delete ps; // delete the string
delete use; // and the counter
}
}
private:
std::string *ps;
int i;
std::size_t *use;
};- 所有的构造函数(除了拷贝构造函数)都需要初始化计数器
Swap
swap 函数的必要性
与拷贝控制成员不同,swap 并不是必要的,但是对于分配了资源的类,
swap
可能是一种很重要的优化手段;一般来说交换两个对象会涉及到一次拷贝,两次赋值;理论上这些内存分配都是不必要的,更希望能直接交换指针,
定义 swap 方法
swap
一般会在类外调用,所以一般会把swap
函数声明为类的友元函数;由于swap
的存在就是为了优化代码的,所以需要定义为inline
函数1
2
3
4
5
6
7
8
9
10
11
12
13
14class HasPtr{
// 友元函数
friend void swap(HasPtr&, HasPtr&);
std::string *ps;
int i;
}
// 由于swap一般是用来优化的,所以一般使用inline函数
inline void swap(HasPtr& lhs, HasPtr& rhs){
// first to check whether there is use defined swap
using std::swap;
swap(lhs.ps, rhs.ps); // ps and i are built-in type, using std::swap
swap(lhs.i, rhs.i);
}调用 swap 方法
一般情况下
swap
调用的是std::swap
,比如内置类型;如果一个类对象有自己类型特定的swap
函数,调用swap
可能会和预期不一致;每个
swap
的调用都应该是不加限定的,对于非内置类型,不建议直接使用std::swap
如果该类型定义了
swap
方法,其匹配优先级会高于std
中定义的版本,会优先调用自定义的版本;如果不存在特定类型的版本,则会使用std
中的版本1
2
3
4
5
6
7
8
9
10
11
12
13
14struct Foo{
HasPtr h;
};
void swap(Foo& lhs, Foo& rhs){
// error, using std::swap rather void swap(HasPtr&, HasPtr&);
std::swap(lhs.h, rhs.h);
}
// correct call
void swap(Foo& lhs, Foo& rhs){
using std::swap;
// call using swap rather std::swap
// though std::swap in scope, void swap(HasPtr&, HasPtr&) is called
swap(lhs.h, rhs.h);
}在赋值运算符中使用 swap
定义了
swap
的类通常使用swap
来定义他的赋值运算符拷贝并交换 (copy and swap),处理了自赋值情况而且天然就是异常安全的;此方法对于拷贝赋值函数是 OK 的,但是对于移动赋值会带来不必要的性能损失
注意此时拷贝构造函数是传值调用,而不是
const reference
1
2
3
4HasPtr& HasPtr::operator=(HasPtr rhs){
swap(*this, rhs);
return *this;
}
Moving Objects
新标准加入了移动
std::move
操作;- 一方面是减少拷贝的消耗,
- 另一方面是为了应付类似于 thread, unique_ptr 等不可拷贝的对象;
- 旧标准里面,容器里面只可以保存可以拷贝的类对象,新标准容器可以保存不可拷贝的对象,只要他们可移动就可以了;
调用
std::move
就说明如果这次将它赋值给别人或者销毁他,后面将不再使用它;不能使用一个移后源对象的值std::move
本身不会对其参数做任何内容上的改变,只是改变了参数的类型,使得构造
和赋值
的时候可以完美匹配移动构造/赋值函数
In particular,
std::move
produces an xvalue expression that identifies its argumentt
. It is exactly equivalent to astatic_cast
to an rvalue reference type.1
static_cast<typename std::remove_reference <T>::type&&>(t)
定义移动构造 (move constructor)/ 移动赋值函数 (move assignment operator)
具体 move 的行为,需要在
move constructor
和move assignment operator
里面定义一般是定义好
move assignment operator
,然后move constructor
里面直接调用move assignment operator
就可以了左值 vs 右值: Understanding-lvalues-and-rvalues-in-C-and-C
左值有持久的状态,是可以取地址或者有名字的;
右值则是临时的,比如除
string literal
之外的literal type
寄存器中计算的中间结果右值变量本身是左值(所有的变量都是左值,因为可以满足第一条)
解引用返回左值,取地址返回右值
Built-in indirection operator
*
The operand of the built-in indirection operator
*
must be pointer to object or a pointer to function, and the result is the lvalue referring to the object or function to which expr points.Built-in address-of operator
&
If the operand is an lvalue expression of some object or function type
T
,operator&
creates and returns a prvalue of typeT*
, with the same cv qualification, that is pointing to the object or function designated by the operand.1
2
3
4
5
6
7int iarray[5] = {1,2,3,4};
// * return lvalue
*(iarray+1) = 5;
for (int i=0; i<5; i++){
std::cout << *(iarray+i) << std::ends;
}
// will print 1 5 3 4 0
左值引用 (lvalue-reference) vs ** 右值引用 (**rvalue-reference)
左值引用就是对一个左值进行引用的类型。右值引用就是对一个右值进行引用的类型,事实上,由于右值通常不具有名字,我们也只能通过引用的方式找到它的存在。
右值引用和左值引用都是属于引用类型。无论是声明一个左值引用还是右值引用,都必须立即进行初始化。而其原因可以理解为是引用类型本身自己并不拥有所绑定对象的内存,只是该对象的一个别名。左值引用是具名变量值的别名,而右值引用则是不具名(匿名)变量的别名。
常量左值引用 (const lvalue reference) 是个 “万能” 的引用类型。它可以接受非常量左值、常量左值、右值对其进行初始化。不过常量左值所引用的右值在它的 “余生” 中只能是只读的。
相对地,非常量左值引用(non const lvalue reference)可以接受非常量左值以及右值对其进行初始化。
右值值引用通常不能绑定到任何的左值(即将左值赋值给右值引用),要想绑定一个左值到右值引用,通常需利用
std::move()
将左值强制转换为右值1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18const int c1 = 1;
int c2 = 2;
int&& rf = 1;
// lvalue reference
const int &a = 123; // rvalue
const int &b = c1; // const lvalue
const int &c = c2; // non-const lvalue
int& lf = rf; // valid, assign rvalue reference to non const lvalue reference,
// reason see quoted section below
const int& clf = rf; // valid, assign rvalue reference to const lvalue reference
// rvalue reference
int &&r1 = c1; // invalid, rvalue reference can not bind to lvalue
int &&r2 = std::move(c1); // valid, std::move convert c1 to a rvalue
int &&r3 = get_int(); // valid when get_int() return non-reference temporary object
int &&r3 = c1 + c2; // valid, c1+c2 return rvalueThe r-value reference is a reference to the original object, so converting it to a l-value reference will just make a reference to the original object.
Once a move constructor is called upon the reference, the original object should be reset to the origin state, and so does any reference to it.1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18string s = "my string";
// call std::move to generate a rvalue reference
string &&rval = move(s);
cout << '"' << rval << '"' << endl; // "my string"
cout << '"' << rval << '"' << endl; // "my string"
cout << '"' << s << '"' << endl; // "my string"
// convert rvalue reference to a lvalue reference
string &lval = rval;
cout << '"' << lval << '"' << endl; // "my string"
// call move constructor
string s2(move(rval));
cout << '"' << rval << '"' << endl; // ""
cout << '"' << lval << '"' << endl; // ""
cout << '"' << s << '"' << endl; // ""
cout << '"' << s2 << '"' << endl; // "my string"右值引用 (
&&
):只能绑定到一个即将销毁的对象,亦即所引用的对象将要被销毁,后续没有其他的用户;- 容器扩容,原来的内存会被释放掉,此时原来的内存就是即将被销毁的对象
- 函数返回非引用类型的临时变量,表达式求的值(关系、位、算术、后置递增递减等)等都是即将销毁的对象
- 手动 std::move 将左值转换为右值
noexcept
move constructor/assignment operator自己定义的移动构造函数 / 移动赋值运算符要加
noexcept
关键字,告诉编译器移动构造不会出问题,也就不需要做出错后备份的操作;否则,比如 vector 在重新分配内存的过程中将旧的内存里面的元素拷贝到新分配的内存的时候,使用的是拷贝构造而不是移动构造;必须在头文件的移动构造函数的声明和定义中都指定 noexcept;原因:如果在重新分配后拷贝原来地址的数据去新地址的过程中,使用了移动构造函数,而移动构造函数又不是 noexcept 的,那么如果在移动了一部分元素而不是全部元素的时候发生了异常,这时候就无法从异常中恢复了,高不成低不就,原地址的数据可能被改变了,新地址的数据又不完整;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49class StrVec{
public:
StrVec() = default;
StrVec(const StrVec&){
std::cout << "StrVec Copy Constructor\n" << std::endl;
}
StrVec& operator=(const StrVec&) noexcept{
std::cout << "StrVec Copy Assignment\n" << std::endl;
}
// difference here
StrVec(StrVec&&) noexcept {
std::cout << "StrVec Move Constructor\n" << std::endl;
}
// difference here
StrVec& operator=(StrVec&&) noexcept{
std::cout << "StrVec Move Assignment\n" << std::endl;
}
};
class StrVecExcept{
public:
StrVecExcept() = default;
StrVecExcept(const StrVecExcept&){
std::cout << "StrVecExcept Copy Constructor\n" << std::endl;
}
StrVecExcept& operator=(const StrVecExcept&) noexcept{
std::cout << "StrVecExcept Copy Assignment\n" << std::endl;
}
// difference here
StrVecExcept(StrVecExcept&&) {
std::cout << "StrVecExcept Move Constructor\n" << std::endl;
}
// difference here
StrVecExcept& operator=(StrVecExcept&&) {
std::cout << "StrVecExcept Move Assignment\n" << std::endl;
}
};
StrVec vec_a;
std::vector<StrVec> vector_str;
vector_str.push_back(std::move(vec_a));
vector_str.push_back(std::move(vec_a));
StrVecExcept vec_e;
std::vector<StrVecExcept> vector_stre;
vector_stre.push_back(std::move(vec_e));
vector_stre.push_back(std::move(vec_e));1
2
3
4
5
6
7StrVec Move Constructor
StrVec Move Constructor
StrVec Move Constructor
StrVecExcept Move Constructor
StrVecExcept Move Constructor
StrVecExcept Copy Constructor // move to newly allocated memory using copy constructor在移动操作之后,源对象必须保持有效的、可析构的状态,但是用户不能对其值进行假定,比如假定是空的;通常我们可以把移动后的源对象进行重置,使其处于一个安全的状态,比如指针置为
nullptr
,vector
执行clear
;以下情况类的
移动构造/赋值函数
会被定义为删除的 (delete
):- 有类成员没有定义自己的移动构造 / 赋值函数(比如定义了拷贝操作但没有定义移动操作或编译器无法为其合成)
- 有类成员显式定义自己的移动构造 / 赋值函数为
delete
- 如果类的析构函数被定义为
delete
的 - 如果有类成员是
const
或者reference
拷贝和移动会相互作用:
- 拷贝构造阻止合成移动构造;只有当一个类没有任何自己版本的拷贝构造函数(即使定义拷贝构造函数为 deleted 的也算是定义了),且他的所有非 static 数据成员都是有移动构造函数或者移动赋值的时候,编译器才会为该类合成移动构造函数或者移动赋值函数(3/5 法则,拷贝构造禁止隐式的生成移动构造函数和移动赋值操作);
- 移动构造阻止合成拷贝构造;如果一个类定义了移动构造函数或者移动赋值函数,同时需要使用拷贝构造 / 赋值函数的话,也必须显式定义自己的拷贝构造函数和拷贝赋值函数,否则该类的合成拷贝构造函数或者拷贝赋值函数默认定义为删除的;
- 如果一个类既有移动构造函数,也有拷贝构造函数,编译器使用普通的函数匹配规则确定使用哪个构造函数;
- 如果一个类定义了拷贝构造函数,没有定义移动构造函数,即使是右值或者 std::move,也会被拷贝;拷贝赋值运算符和移动赋值运算符也是类似的;
- 3/5 法则:所有的 5 个拷贝控制成员应该看成一个整体,一般来说,如果一个类定义了任何一个拷贝操作,他就应该定义所有的 5 个操作;
拷贝并交换的赋值运算符
接受一个非引用参数,依赖于实参的类型,拷贝初始化遵循 “左值被拷贝,右值被移动” 的原则,单一的赋值运算符就实现了拷贝赋值运算和移动赋值运算;但是这样实现的移动赋值性能不好,因为多了一次参数的拷贝,而移动本身是不需要这样一次拷贝的;
1
2
3
4
5
6class HasPtr{
HasPtr& operatpr=(HasPtr ptr){ // 多了一次参数的拷贝
swap(*this, ptr);
return *this;
}
};移动迭代器 (move iterator)
新标准定义了移动迭代器。一般来说,一个迭代器解引用得到的是一个左值,但是一个移动迭代器解引用得到的是一个右值;通过
make_move_iterator
来将一个普通迭代器转化为移动迭代器1
2
3auto last = uninitialized_copy(std::make_move_iterator(begin()),
std::make_move_iterator(end()),
first); // 使用移动构造函数来构造元素unique_ptr
是不能拷贝的,但是当把unique_ptr
当作返回值返回的时候,因为这时候他即将要被销毁,所以触发了unique_ptr
的移动构造函数,而不是拷贝构造函数,所以是合法的;1
2
3
4std::unique_ptr<int> clone(int p){
std::unique_ptr<int> ret(new int(p));
retutn ret;
}区分移动和拷贝的重载函数
通常拷贝的版本接受
const T&
形参,移动的版本接受T&&
作为形参const reference
可以接受任何可以转换为 X 的对象。但是如果传递的是non-const rvalue reference
,是和移动的版本精准匹配的,就不会调用拷贝版本的函数(转换为这个版本的需要加上底层 const,不是最精准匹配,详见 chapter6)- 第二个版本只能接受
non-const rvalue reference
一般来说,不需要定义接受
non-const lvaue reference
的版本,因为non-const
是可以自动转换为const
的也不需要定义
const rvalue reference
,因为既然作为右值传过来,就是马上要销毁的,就得是non-const
的1
2
3
4class StrVec{
void push_back(const std::string &);
void push_back(std::string &&);
};引用限定符 (reference qualifier)
可以使用引用限定对类的成员函数进行限定,指出
this
可以指向一个左值或者一个右值:对于
&
限定的函数,只能将它指向左值,即该函数只可以被左值调用;1
2
3
4
5
6struct X {
void foo() & {}
};
// 编译器也会提示 passing ‘X’ as ‘this’ argument discards qualifiers
X().foo();对于
&&
限定的函数,只能将它指向右值,即该函数只可以被右值调用;1
2
3
4
5
6
7struct X {
void foo() && {}
};
// 编译器会提示 error: passing ‘X’ as ‘this’ argument discards qualifiers [-fpermissive]
X x;
x.foo();一个函数可以同时被 const 和引用限定符修饰,这时候引用修饰符应该在 const 后面 ; 类似于 noexcept,引用限定符必须同时出现在函数的声明和定义里面;
1
2
3
4struct X {
void foo() const &; // 正确.
void bar() & const; // 错误!
};
引用限定符的重载
类似于有无 const 的重载,可以通过引用限定符的重载来实现不同的对象类型调用不同的函数
1
2
3
4
5
6
7
8
9
10
11class Foo{
public:
Foo sorted() &&; //可以用于可改变的右值
Foo sorted() &; //可用于任何类型的Foo
};
// 假设:
// retFoo(); // 返回左值
// retVal(); // 返回右值
retVal().sorted(); // 调用Foo sorted() &&;
retFoo().sorted(); // 调用Foo sorted() &;如果一个成员函数有引用限定符,则具有相同参数列表的函数也必须有引用限定符;
1
2
3
4
5
6
7class A {
public:
void foo() & { std::cout << "A::foo()\n"; }
void foo() && { std::cout << "A::foo()\n"; }
void foo(int a) { std::cout << "A::foo()\n"; }
void foo() { std::cout << "A::foo()\n"; } // invalid
};1
2error: cannot overload a member function without a ref-qualifier with a member function with ref-qualifier '&'
void foo() { std::cout << "A::foo()\n"; }
Value Category
-
C++98 中右值是纯右值,纯右值指的是临时变量值、不跟对象关联的字面量值。临时变量指的是非引用返回的函数返回值、表达式等,例如函数 int func () 的返回值,表达式 a+b;不跟对象关联的字面量值,例如 true,2,”C” 等。
C++11 对 C++98 中的右值进行了扩充。在 C++11 中右值又分为纯右值(prvalue,pure rvalue)和将亡值(xvalue,eXpiring value)。其中纯右值的概念等同于我们在 C++98 标准中右值的概念,指的是临时变量和不跟对象关联的字面量值;将亡值则是 C++11 新增的跟右值引用相关的表达式,这样表达式通常是将要被移动的对象(移为他用),比如返回右值引用 T&& 的函数返回值、std::move 的返回值,或者转换为 T&& 的类型转换函数的返回值。
将亡值可以理解为通过 “盗取” 其他变量内存空间的方式获取到的值。在确保其他变量不再被使用、或即将被销毁时,通过 “盗取” 的方式可以避免内存空间的释放和分配,能够延长变量值的生命期。
reference: https://blog.csdn.net/hyman_yx/article/details/52044632Each C++ expression (an operator with its operands, a literal, a variable name, etc.) is characterized by two independent properties: a type and a value category. Each expression has some non-reference type, and each expression belongs to exactly one of the three primary value categories: prvalue, xvalue, and lvalue.
a glvalue (“generalized” lvalue) is an expression whose evaluation determines the identity of an object or function;
a prvalue (“pure” rvalue) is an expression whose evaluation
computes the value of an operand of a built-in operator (such prvalue has no result object), or
initializes an object (such prvalue is said to have a result object).
The result object may be a variable, an object created by new-expression, a temporary created by temporary materialization, or a member thereof. Note that non-void discarded expressions have a result object (the materialized temporary). Also, every class and array prvalue has a result object except when it is the operand of decltype;
an xvalue (an “eXpiring” value) is a glvalue that denotes an object whose resources can be reused;
a lvalue (so-called, historically, because lvalues could appear on the left-hand side of an assignment expression) is a glvalue that is not an xvalue;
a rvalue (so-called, historically, because rvalues could appear on the right-hand side of an assignment expression) is a prvalue or an xvalue.
Note: this taxonomy went through significant changes with past C++ standard revisions, see History below for details.
glvalue
A glvalue expression is either lvalue or xvalue.
Properties:
- A glvalue may be implicitly converted to a prvalue with lvalue-to-rvalue, array-to-pointer, or function-to-pointer implicit conversion.
- A glvalue may be polymorphic: the dynamic type of the object it identifies is not necessarily the static type of the expression.
- A glvalue can have incomplete type, where permitted by the expression.
rvalue
An rvalue expression is either prvalue or xvalue.
Properties:
- Address of an rvalue cannot be taken by built-in address-of operator:
&int()
,&i++
[3],&42
, and&std::move(x)
are invalid. - An rvalue can’t be used as the left-hand operand of the built-in assignment or compound assignment operators.
- An rvalue may be used to initialize a const lvalue reference, in which case the lifetime of the object identified by the rvalue is extended until the scope of the reference ends.
- An rvalue may be used to initialize an rvalue reference, in which case the lifetime of the object identified by the rvalue is extended until the scope of the reference ends.
- An rvalue may be used to initialize an rvalue reference, in which case the lifetime of the object identified by the rvalue is extended until the scope of the reference ends.
lvalue
The following expressions are lvalue expressions:
- the name of a variable, a function, a template parameter object (since C++20), or a data member, regardless of type, such as
std::cin)
orstd::endl
. Even if the variable’s type is rvalue reference, the expression consisting of its name is an lvalue expression; - a function call or an overloaded operator expression, whose return type is lvalue reference, such as
std::getline(std::cin, str)
,std::cout << 1
,str1 = str2
, or++it
; a = b
,a += b
,a %= b
, and all other built-in assignment and compound assignment expressions;++a
and--a
, the built-in pre-increment and pre-decrement expressions;*p
, the built-in indirection expression;a[n]
andp[n]
, the built-in subscript expressions, where one operand ina[n]
is an array lvalue (since C++11);a.m
, the member of object expression, except wherem
is a member enumerator or a non-static member function, or wherea
is an rvalue andm
is a non-static data member of object type;p->m
, the built-in member of pointer expression, except wherem
is a member enumerator or a non-static member function;a.*mp
, the pointer to member of object expression, wherea
is an lvalue andmp
is a pointer to data member;p->*mp
, the built-in pointer to member of pointer expression, wheremp
is a pointer to data member;a, b
, the built-in comma expression, whereb
is an lvalue;a ? b : c
, the ternary conditional expression for certainb
andc
(e.g., when both are lvalues of the same type, but see definition for detail);- a string literal, such as
"Hello, world!"
; - a cast expression to lvalue reference type, such as
static_cast<int&>(x)
;
Properties:
- Same as glvalue (above).
- Address of an lvalue may be taken by built-in address-of operator:
&++i
[1] and&std::endl
are valid expressions. - A modifiable lvalue may be used as the left-hand operand of the built-in assignment and compound assignment operators.
- An lvalue may be used to initialize an lvalue reference; this associates a new name with the object identified by the expression.
prvalue
The following expressions are prvalue expressions:
- a literal (except for string literal), such as
42
,true
ornullptr
; - a function call or an overloaded operator expression, whose return type is non-reference, such as
str.substr(1, 2)
,str1 + str2
, orit++
; a++
anda-
-, the built-in post-increment and post-decrement expressions;a + b
,a % b
,a & b
,a << b
, and all other built-in arithmetic expressions;a && b
,a || b
,!a
, the built-in logical expressions;a < b
,a == b
,a >= b
, and all other built-in comparison expressions;&a
, the built-in address-of expression;a.m
, the member of object expression, wherem
is a member enumerator or a non-static member function[2], or wherea
is an rvalue andm
is a non-static data member of non-reference type (until C++11);p->m
, the built-in member of pointer expression, wherem
is a member enumerator or a non-static member function[2];a.*mp
, the pointer to member of object expression, wheremp
is a pointer to member function[2], or wherea
is an rvalue andmp
is a pointer to data member (until C++11);p->*mp
, the built-in pointer to member of pointer expression, wheremp
is a pointer to member function[2];a, b
, the built-in comma expression, whereb
is an rvalue;a ? b : c
, the ternary conditional expression for certainb
andc
(see definition for detail);- a
cast
expression to non-reference type, such asstatic_cast<double>(x)
,std::string{}
, or(int)42
; - the this pointer;
- an enumerator;
- non-type template parameter unless its type is a class or (since C++20) an lvalue reference type;
- a lambda expression, such as
[](int x){ return x * x; };
(since C++11) - a requires-expression, such as
requires (T i) { typename T::type; };
- a specialization of a concept, such as
std::equality_comparable<int>
Properties:
- Same as rvalue (above).
- A prvalue cannot be polymorphic: the dynamic type of the object it denotes is always the type of the expression.
- A non-class non-array prvalue cannot be cv-qualified, unless it is materialized in order to be bound to a reference to a cv-qualified type (since C++17). (Note: a function call or cast expression may result in a prvalue of non-class cv-qualified type, but the cv-qualifier is generally immediately stripped out.)
- A prvalue cannot have incomplete type (except for type void, see below, or when used in
decltype
specifier) - A prvalue cannot have abstract class type or an array thereof.
xvalue
The following expressions are xvalue expressions:
- a function call or an overloaded operator expression, whose return type is rvalue reference to object, such as
std::move(x)
; a[n]
, the built-in subscript expression, where one operand is an array rvalue;a.m
, the member of object expression, wherea
is an rvalue andm
is a non-static data member of non-reference type;a.*mp
, the pointer to member of object expression, wherea
is an rvalue andmp
is a pointer to data member;a ? b : c
, the ternary conditional expression for certainb
andc
(see definition for detail);- a
cast
expression to rvalue reference to object type,such as static_cast<char&&>(x);
- any expression that designates a temporary object, after temporary materialization.
Properties:
- Same as rvalue (above).
- Same as glvalue (above).
In particular, like all rvalues, xvalues bind to rvalue references, and like all glvalues, xvalues may be polymorphic, and non-class xvalues may be cv-qualified.