深入解析C++右值引用和移动语义:编写更快、更节省内存的代码_c++ 右值引用 移动语义-程序员宅基地

技术标签: 算法  Linux后台开发技术  c++  linux  服务器  STL  C/C++技术干货  


C++11中引用了右值引用和移动语义,可以避免无谓的复制,提高程序性能。

一、左值和右值

左值可以取地址,位于等号左边。
右值无法取地址,位于等号右边。

int a=10;

a可以通过&取地址,位于等号左边,所以a是左值。10位于等号右边且无法通过&取地址,所以10是右值。

struct A{
    
	A(int a=0)
	{
    
		a_=a;
	}
	int a_;
};

A a=A();

同样的,a可以通过 & 取地址,位于等号左边,所以a是左值。
A()是个临时值,没法通过 & 取地址,位于等号右边,所以A()是个右值。

可见,有地址的变量就是左值,没有地址的字面值、临时值就是右值。

二、左值引用

引用本质是别名,可以通过引用修改变量的值,传参时传引用可以避免拷贝。
定义:能指向左值,不能指向右值的就是左值引用。

#include <iostream>

int main(int argc, char **argv)
{
    
	int a = 10;
	int &ref_a = a;
	int &ref_b = 10; // 左值引用指向了右值,会编译失败
	return 0;
}

左值引用指向了右值时编译出现报错:

error: invalid initialization of non-const reference of type ‘int&’ from an rvalue of type ‘int’

引用是变量的别名,由于右值没有地址,没法被修改,所以左值引用无法指向右值。

但是,const左值引用是可以指向右值的。

const int &ref_b = 10;// 编译通过

const左值引用不会修改指向值。因此可以指向右值,这也是为什么要使用 const & 作为函数参数的原因之一,如 std::vector 的 push_back 。

void push_back (const value_type& val);

如果没有 const , vec.push_back(5) 这样的代码就无法编译通过。

三、右值引用

再看下右值引用,右值引用的标志是 && ,顾名思义,右值引用专门为右值而生,可以指向右值,不能指向左值
右值引用的用途:可以修改右值。

#include <iostream>

int main(int argc, char **argv)
{
    
	int &&ref_a_right = 10;//编译通过
	
	int a = 5;

	int &&ref_a_left = a;// 编译不过,右值引用不可以指向左值
	
	ref_a_right = 16; // 右值引用的用途:可以修改右值

	return 0;
}

右值引用指向左值时编译报错:

error: cannot bind ‘int’ lvalue to ‘int&&

四、左右值引用的本质

引用的本质就是指向目标地址来获得资源。

4.1、右值引用指向左值的办法

通过std::move()可以将右值引用指向左值。

#include <iostream>
using namespace std;
int main(int argc, char **argv)
{
    	
	int a = 5;// a是左值

	int &ref_a_left = a; // 左值引用
	
	int &&ref_a_right = std::move(a);//右值引用指向左值,编译通过

	cout << "a=" << a << endl;

	cout << "ref_a_left=" << ref_a_left << endl;
	cout << "ref_a_right=" << ref_a_right << endl;

	return 0;
}

在上边的代码里,看上去是左值a通过std::move移动到了右值ref_a_right中,那是不是a里边就没有值了?并不是,打印出a的值仍然是5。
执行结果:

a=5
ref_a_left=5
ref_a_right=5

std::move()是一个非常有迷惑性的函数:
(1)不理解左右值概念的往往以为它能把一个变量里的内容移动到另一个变量;
(2)事实上std::move()移动不了什么,唯一的功能是把左值强制转化为右值,让右值引用可以指向左值。其实现等同于一个类型转换: static_cast<T&&>(lvalue) 。 所以,单纯的std::move(xxx)不会有性能提升。

同样的,右值引用能指向右值,本质上也是把右值提升为一个左值,并定义一个右值引用通过std::move指向该左值。

#include <iostream>
using namespace std;
int main(int argc, char **argv)
{
    	
	int a = 5;

	int &ref_a_left = a;
	
	int &&ref_a_right = std::move(a);//编译通过

	ref_a_right = 7;

	cout << "&a=" << &a << "\n&ref_a_left=" << &ref_a_left << "\n&ref_a_right=" << &ref_a_right << endl;

	cout << "a=" << a << endl;

	cout << "ref_a_left=" << ref_a_left << endl;
	cout << "ref_a_right=" << ref_a_right << endl;

	return 0;
}

此时a等于多少呢?
运行结果如下,地址没有变,值改变了。

&a=0x7ffd7bd6ea54
&ref_a_left=0x7ffd7bd6ea54
&ref_a_right=0x7ffd7bd6ea54
a=7
ref_a_left=7
ref_a_right=7

4.2、左值引用、右值引用本身是左值还是右值?

被声明出来的左、右值引用都是左值。 因为被声明出的左右值引用是有地址的,也位于等号左边。看下边代码:

// 形参是个右值引用 
void change(int&& right_value) 
{
     
	right_value = 8; 
}
int main() 
{
     
	int a = 5; // a是个左值 
	int &ref_a_left = a; // ref_a_left是个左值引用 
	int &&ref_a_right = std::move(a); // ref_a_right是个右值引用 

	//change(a); // 编译不过,a是左值,change参数要求右值 
	//change(ref_a_left); // 编译不过,左值引用ref_a_left本身也是个左值 
	//change(ref_a_right); // 编译不过,右值引用ref_a_right本身也是个左值 

	change(std::move(a)); // 编译通过 
	change(std::move(ref_a_right)); // 编译通过 
	change(std::move(ref_a_left)); // 编译通过 
	change(5); // 当然可以直接接右值,编译通过 
	cout << &a << ' '; cout << &ref_a_left << ' '; cout << &ref_a_right << endl; 
	// 打印这三个左值的地址,都是一样的 
}

std::move会返回一个右值引用 int && ,它是左值还是右值呢? 从表达式 int &&ref = std::move(a) 来看,右值引用 ref 指向的必须是右值,所以move返回的 int && 是个右值。
所以右值引用既可能是左值,又可能是右值吗? 确实如此,右值引用既可以是左值也可以是右值,如果有名称则为左值,否则是右值。
或者说:作为函数返回值的 && 是右值,直接声明出来的 && 是左值。 这同样也符合前面章节对左值,右值的判定方式:其实引用和普通变量是一样的, int &&ref = std::move(a) 和 int a = 5 没有什么区别,等号左边就是左值,右边就是右值。

从上述分析中得到如下结论:

  1. 从性能上讲,左右值引用没有区别,传参使用左右值引用都可以避免拷贝。
  2. 右值引用可以直接指向右值,也可以通过std::move指向左值;而左值引用只能指向左值(const左值引用也能指向右值)。
  3. 作为函数形参时,右值引用更灵活。虽然const左值引用也可以做到左右值都接受,但它无法修改,有一定局限性。
void f(const int& n) 
{
     
	n += 1; // 编译失败,const左值引用不能修改指向变量 
}

void f2(int && n) 
{
     
	n += 1; // ok 
}

int main() 
{
     
	f(5); 
	f2(5); 
}

五、右值引用和std::move使用场景

std::move 只是类型转换工具,不会对性能有好处;右值引用在作为函数形参时更具灵活性。

5.1、右值引用优化性能,避免深拷贝

(1)浅拷贝重复释放。
对于含有堆内存的类,我们需要提供深拷贝的拷贝构造函数,如果使用默认构造函数,会导致堆内存的重复删除,比如下面的代码:

#include <iostream>
using namespace std;

class A {
    
public:
	A() :m_ptr(new int(0))
	{
    
		cout << "constructor A" << endl;
	}
	~A()
	{
    
		cout << "destructor A, m_ptr:" << m_ptr << endl;
		delete m_ptr;
		m_ptr = nullptr;
	}
private:
	int *m_ptr;
};

// 为了避免返回值优化,此函数故意这样写 
A Get(bool flag)
{
    
	A a;
	A b;
	cout << "ready return" << endl;
	if (flag)
		return a;

	return b;
}

int main(int argc, char **argv)
{
    	
	{
    
		A a = Get(false);
	}

	cout << "main finish" << endl;

	return 0;
}

运行报错:

constructor A
constructor A
ready return
destructor A, m_ptr:0xe4b2a0
destructor A, m_ptr:0xe4ae70
destructor A, m_ptr:0xe4b2a0
free(): double free detected in tcache 2
已放弃 (核心已转储)

(2)深拷贝构造函数。
在上面的代码中,默认构造函数是浅拷贝,main函数的 a 和Get函数的 b 会指向同一个指针 m_ptr,在析构的时候会导致重复删除该指针。正确的做法是提供深拷贝的拷贝构造函数,比如下面的代码:

#include <iostream>
using namespace std;

class A {
    
public:
	A() :m_ptr(new int(0))
	{
    
		cout << "constructor A" << endl;
	}
	A(const A& a) :m_ptr(new int(*a.m_ptr))
	{
    
		cout << "copy constructor A" << endl;
	}
	~A()
	{
    
		cout << "destructor A, m_ptr:" << m_ptr << endl;
		delete m_ptr;
		m_ptr = nullptr;
	}
private:
	int *m_ptr;
};

// 为了避免返回值优化,此函数故意这样写 
A Get(bool flag)
{
    
	A a;
	A b;
	cout << "ready return" << endl;
	if (flag)
		return a;

	return b;
}

int main(int argc, char **argv)
{
    	
	{
    
		A a = Get(false);
	}

	cout << "main finish" << endl;

	return 0;
}

执行结果:

constructor A
constructor A
ready return
copy constructor A
destructor A, m_ptr:0x184e2a0
destructor A, m_ptr:0x184de70
destructor A, m_ptr:0x184e2c0
main finish

(3)移动构造函数。
这样深拷贝构造函数就可以保证拷贝构造时的安全性,但有时这种拷贝构造却是不必要的,比如上面代码中的拷贝构造就是不必要的。上面代码中的 Get 函数会返回临时变量,然后通过这个临时变量拷贝构造了一个新的对象 b,临时变量在拷贝构造完成之后就销毁了,如果堆内存很大,那么,这个拷贝构造的代价会很大,带来了额外的性能损耗。有没有办法避免临时对象的拷贝构造呢?看下面的代码:

#include <iostream>
using namespace std;

class A {
    
public:
	A() :m_ptr(new int(0))
	{
    
		cout << "constructor A" << endl;
	}
	A(const A& a) :m_ptr(new int(*a.m_ptr))
	{
    
		cout << "copy constructor A" << endl;
	}
	//  移动构造函数,可以浅拷贝
	A(A&& a):m_ptr(a.m_ptr)
	{
    
		// 为防止a析构时delete data,提前置空其m_ptr
		a.m_ptr = nullptr;
		cout << "move constructor A" << endl;
	}
	~A()
	{
    
		cout << "destructor A, m_ptr:" << m_ptr << endl;
		delete m_ptr;
		m_ptr = nullptr;
	}
private:
	int *m_ptr;
};

// 为了避免返回值优化,此函数故意这样写 
A Get(bool flag)
{
    
	A a;
	A b;
	cout << "ready return" << endl;
	if (flag)
		return a;

	return b;
}

int main(int argc, char **argv)
{
    	
	{
    
		A a = Get(false);
	}

	cout << "main finish" << endl;

	return 0;
}

上面的代码中没有了拷贝构造,取而代之的是移动构造( Move Construct)。从移动构造函数的实现中可以看到,它的参数是一个右值引用类型的参数 A&&,这里没有深拷贝,只有浅拷贝,这样就避免了对临时对象的深拷贝,提高了性能。这里的 A&& 用来根据参数是左值还是右值来建立分支,如果是临时值,则会选择移动构造函数。移动构造函数只是将临时对象的资源做了浅拷贝,不需要对其进行深拷贝,从而避免了额外的拷贝,提高性能。这也就是所谓的移动语义( move 语义),右值引用的一个重要目的是用来支持移动语义的

执行结果:

constructor A
constructor A
ready return
move constructor A
destructor A, m_ptr:0
destructor A, m_ptr:0x1eb9e70
destructor A, m_ptr:0x1eba2a0
main finish

移动语义可以将资源(堆、系统对象等)通过浅拷贝方式从一个对象转移到另一个对象,这样能够减少不必要的临时对象的创建、拷贝以及销毁,可以大幅度提高 C++ 应用程序的性能,消除临时对象的维护(创建和销毁)对性能的影响。

5.2、移动(move)语义

move是将对象的状态或者所有权从一个对象转移到另一个对象,只是转义,没有内存拷贝。要move语义起作用,核心在于需要对应类型的构造函数支持。

移动构造
move
resources
SourceObject
DestObject
拷贝构造
copy
resources
SourceObject
DestObject
resources
#include <iostream> 
#include <vector> 
#include <cstdio> 
#include <cstdlib> 
#include <string.h>

using namespace std;

class MyString
{
    
public:
	MyString()
	{
    
		m_data = NULL;
		m_len = 0;
	}
	MyString(const char *p)
	{
    
		m_len = strlen(p);
		copy_data(p);
	}
	MyString(const MyString& str)
	{
    
		m_len = str.m_len; 
		copy_data(str.m_data); 
		std::cout << "Copy Constructor is called! source: " << str.m_data << std::endl;
	}
	MyString& operator=(const MyString& str)
	{
    
		if (this != &str)
		{
    
			m_len = str.m_len;
			copy_data(str.m_data);
		}
		std::cout << "Copy Assignment is called! source: " << str.m_data << std::endl; 
		return *this;
	}
	// 用c++11的右值引用来定义这两个函数
	MyString(MyString&& str) 
	{
    
		std::cout << "Move Constructor is called! source: " << str.m_data << std::endl;
		m_len = str.m_len; 
		m_data = str.m_data; //避免了不必要的拷贝 
		str.m_len = 0; 
		str.m_data = NULL;
	}
	MyString& operator=(MyString&& str)
	{
    
		std::cout << "Move Assignment is called! source: " << str.m_data << std::endl;
		if (this != &str)
		{
    
			m_len = str.m_len;
			m_data = str.m_data; //避免了不必要的拷贝 
			str.m_len = 0;
			str.m_data = NULL;
		}
		return *this;
	}
	virtual ~MyString()
	{
    
		if (m_data) 
			free(m_data);
	}

private:
	char * m_data;
	size_t m_len;
	void copy_data(const char *s)
	{
    
		m_data = new char[m_len + 1];
		memcpy(m_data, s, m_len);
		m_data[m_len] = '\0';
	}
};


int main()
{
    
	MyString a;
	a = MyString("Hello");// Move Assignment
	MyString b = a;// Copy Constructor
	MyString c = std::move(a);// Move Constructor is called! 将左值转为右值
	
	std::vector<MyString> vec; 
	vec.push_back(MyString("World")); // Move Constructor is called!
	
	return 0;
}

执行结果:

Move Assignment is called! source: Hello
Copy Constructor is called! source: Hello
Move Constructor is called! source: Hello
Move Constructor is called! source: World

有了右值引用和转移语义,我们在设计和实现类时,对于需要动态申请大量资源的类,应该设计右值引用的拷贝构造函数和赋值函数,以提高应用程序的效率。

5.3、forward 完美转发

forward 完美转发实现了参数在传递过程中保持其值属性的功能,即若是左值,则传递之后仍然是左值,若是右值,则传递之后仍然是右值。

现存在一个函数。

Template<class T> 
void func(T &&val);

根据前面所描述的,这种引用类型既可以对左值引用,亦可以对右值引用。但要注意,引用以后,这个val值它本质上是一个左值
看下面例子:

int &&a = 10; 
int &&b = a; //错误

a是一个右值引用,但其本身a也有内存名字,所以a本身是一个左值,再用右值引用引用a这是不对的。

因此有了std::forward()完美转发,这种T &&val中的val是左值,但如果用std::forward (val),就会按照参数原来的类型转发。

int &&a = 10; 
int &&b = std::forward<int>(a);

示例:

#include <iostream> 
using namespace std;

template <class T> 
void Print(T &t) 
{
     
	cout << "L" << t << endl; 
}
template <class T> 
void Print(T &&t) 
{
     
	cout << "R" << t << endl; 
}
template <class T> 
void func(T &&t) 
{
     
	Print(t); 
	Print(std::move(t)); 
	Print(std::forward<T>(t)); 
}

int main() {
    
	cout << "-- func(1)" << endl; 
	func(1); 
	int x = 10; 
	int y = 20; 
	cout << "-- func(x)" << endl; 
	func(x); // x本身是左值 
	cout << "-- func(std::forward<int>(y))" << endl; 
	func(std::forward<int>(y)); 

	cout << "-- func(std::forward<int&>(y))" << endl;
	func(std::forward<int&>(y));
	return 0; 
}

执行结果:

-- func(1)
L1
R1
R1
-- func(x)
L10
R10
L10
-- func(std::forward<int>(y))
L20
R20
R20
-- func(std::forward<int&>(y))
L20
R20
L20

分析: func(1)由于1是右值,所以未定的引用类型T&&v被一个右值初始化后变成了一个右值引用,但是在func()函数体内部,调用PrintT(v) 时,v又变成了一个左值(因为在std::forward里它已经变成了一个具名的变量,所以它是一个左值),因此,示例测试结果第一个PrintT被调用,打印出“L1"调用PrintT(std::forward(v))时,由于std::forward会按参数原来的类型转发,因此,它还是一个右值(这里已经发生了类型推导,所以这里的T&&不是一个未定的引用类型,会调用void PrintT(T&&t)函
数打印 “R1”.调用PrintT(std::move(v))是将v变成一个右值(v本身也是右值),因此,它将输出”R1"func(x)未定的引用类型T&&v被一个左值初始化后变成了一个左值引用,因此,在调用PrintT(std::forward(v))时它会被转发到void PrintT(T&t)。

5.4、综合示例

#include "stdio.h" 
#include <iostream> 
#include <cstring> 
#include <vector>

using namespace std;

class A
{
    
public:
	int *m_ptr = NULL; // 增加初始化 
	int m_nSize = 0;

	A() :m_ptr(NULL), m_nSize(0) {
    }
	A(int *ptr, int nSize)
	{
    
		m_nSize = nSize;
		m_ptr = new int[nSize];
		printf("A(int *ptr, int nSize) m_ptr:%p\n", m_ptr);
		if (m_ptr)
		{
    
			memcpy(m_ptr, ptr, sizeof(sizeof(int) * nSize));
		}
	}
	A(const A&other)// 拷贝构造函数实现深拷贝
	{
    
		m_nSize = other.m_nSize;
		if (other.m_ptr)
		{
    
			printf("A(const A &other) m_ptr:%p\n", m_ptr);
			if (m_ptr)
				delete[] m_ptr;
			printf("delete[] m_ptr\n");

			m_ptr = new int[m_nSize];
			memcpy(m_ptr, other.m_ptr, sizeof(sizeof(int) * m_nSize));
		}
		else
		{
    
			if (m_ptr)
				delete[] m_ptr;
			m_ptr = NULL;
		}
		cout << "A(const int &i)" << endl;
	}
	// 右值应用构造函数
	A(A &&other)
	{
    
		m_ptr = NULL;
		m_nSize = other.m_nSize;
		if (other.m_ptr)
		{
    
			m_ptr = move(other.m_ptr);
			other.m_ptr = NULL;
		}
	}
	
	~A()
	{
    
		if (m_ptr)
		{
    
			delete[] m_ptr;
			m_ptr = NULL;
		}
	}
	void deleteptr() 
	{
     
		if (m_ptr) 
		{
     
			delete[] m_ptr; 
			m_ptr = NULL; 
		} 
	}

};

int main() {
    
	int arr[] = {
     1, 2, 3 }; 
	A a(arr, sizeof(arr) / sizeof(arr[0])); 
	cout << "m_ptr in a Addr: 0x" << a.m_ptr << endl; 
	A b(a); 
	cout << "m_ptr in b Addr: 0x" << b.m_ptr << endl; b.deleteptr(); 
	A c(std::forward<A>(a)); // 完美转换 
	cout << "m_ptr in c Addr: 0x" << c.m_ptr << endl; c.deleteptr(); 
	vector<int> vect{
    1, 2, 3, 4, 5}; 
	cout << "before move vect size: " << vect.size() << endl; 
	vector<int> vect1 = move(vect); 
	cout << "after move vect size: " << vect.size() << endl; 
	cout << "new vect1 size: " << vect1.size() << endl; 
	return 0; 
}

执行结果:

A(int *ptr, int nSize) m_ptr:0x219ce70
m_ptr in a Addr: 0x0x219ce70
A(const A &other) m_ptr:(nil)
delete[] m_ptr
A(const int &i)
m_ptr in b Addr: 0x0x219d2a0
m_ptr in c Addr: 0x0x219ce70
before move vect size: 5
after move vect size: 0
new vect1 size: 5

5.5、emplace_back 减少内存拷贝和移动

对于STL容器,C++11后引入了emplace_back接口。
emplace_back是就地构造,不用构造后再次复制到容器中。因此效率更高。

考虑这样的语句:

vector<string> testVec;
testVec.push_back(string(16,'q'));

上述语句足够简单易懂,将一个string对象添加到testVec中。底层实现:
(1)首先,string(16, ‘a’)会创建一个string类型的临时对象,这涉及到一次string构造过程。
(2)其次,vector内会创建一个新的string对象,这是第二次构造。
(3)最后在push_back结束时,最开始的临时对象会被析构。加在一起,这两行代码会涉及到两次string构造和一次析构。

c++11可以用emplace_back代替push_back,emplace_back可以直接在vector中构建一个对象,而非创建一个临时对象,再放进vector,再销毁。emplace_back可以省略一次构建和一次析构,从而达到优化的目的。

测试示例:

#include <iostream> 
#include <memory> 
#include <string> 
#include <vector>
#ifdef GCC 
#include <sys/time.h> 
#else 
#include <ctime> 
#endif 
// GCC 
class TimeInterval 
{
    
public: 
	TimeInterval(const std::string& d) : detail(d) 
	{
     
		init(); 
	}
	TimeInterval() 
	{
     
		init(); 
	}
	~TimeInterval() 
	{
    
#ifdef GCC
		gettimeofday(&end, NULL); 
		std::cout << detail << 1000 * (end.tv_sec - start.tv_sec) + (end.tv_usec - start.tv_usec) / 1000 << " ms" << endl; 
#else 
		end = clock(); 
		std::cout << detail << (double)(end - start) << " ms" << std::endl; 
#endif // GCC 
	} 
protected: 
	void init() 
	{
     
#ifdef GCC
		gettimeofday(&start, NULL); 
#else 
		start = clock(); 
#endif // GCC 
	} 
private: 
	std::string detail; 
#ifdef GCC 
	timeval start, end; 
	#else
	clock_t start, end; 
#endif // GCC 
};

#define TIME_INTERVAL_SCOPE(d) std::shared_ptr<TimeInterval> time_interval_scope_begin = std::make_shared<TimeInterval>(d)


int main() {
    
	std::vector<std::string> v; 
	int count = 100000; 
	v.reserve(count); //预分配十万大小,排除掉分配内存的时间 
	{
     
		TIME_INTERVAL_SCOPE("push_back string:"); 
		for (int i = 0; i < count; i++) 
		{
     
			std::string temp("test"); 
			v.push_back(temp);// push_back(const string&),参数是左值引用 
		} 
	}
	v.clear(); 
	{
     
		TIME_INTERVAL_SCOPE("push_back move(string):");
		for (int i = 0; i < count; i++) 
		{
    
			std::string temp("test"); 
			v.push_back(std::move(temp));// push_back(string &&), 参数是右值引用 
		} 
	}
	v.clear(); 
	{
     
		TIME_INTERVAL_SCOPE("push_back(string):"); 
		for (int i = 0; i < count; i++) 
		{
     
			v.push_back(std::string("test"));// push_back(string &&), 参数是右值引 用 
		} 
	}
	v.clear(); 
	{
     
		TIME_INTERVAL_SCOPE("push_back(c string):"); 
		for (int i = 0; i < count; i++) 
		{
     
			v.push_back("test");// push_back(string &&), 参数是右值引用 
		} 
	}
	v.clear(); 
	{
     
		TIME_INTERVAL_SCOPE("emplace_back(c string):"); 
		for (int i = 0; i < count; i++) 
		{
     
			v.emplace_back("test");// 只有一次构造函数,不调用拷贝构造函数,速度最快 
		} 
	} 
}

执行结果:

push_back string:2748 ms
push_back move(string):1991 ms
push_back(string):1940 ms
push_back(c string):1974 ms
emplace_back(c string):1501 ms

分析:
第1中方法耗时最长,原因显而易见,将调用左值引用的push_back,且将会调用一次string的拷贝构造函数,比较耗时,这里的string还算很短的,如果很长的话,差异会更大。

第2、3、4中方法耗时基本一样,参数为右值,将调用右值引用的push_back,故调用string的移动构造函数,移动构造函数耗时比拷贝构造函数少,因为不需要重新分配内存空间。

第5中方法耗时最少,因为emplace_back只调用构造函数,没有移动构造函数,也没有拷贝构造函数。

总结

C++11 在性能上做了很大的改进,最大程度减少了内存移动和复制,通过右值引用、 forward、emplace 和一些无序容器我们可以大幅度改进程序性能。

  1. std::move强制转换为右值引用,触发移动构造函数,它的语法有&&。移动构造函数优先级高于拷贝构造函数。
  2. 自己实现的类,要具备移动语义,必须自己实现移动构造函数。
  3. 右值引用仅仅是通过改变资源的所有者来避免内存的拷贝,能大幅度提高性能。
  4. forward 能根据参数的实际类型转发给正确的函数。
  5. emplace 系列函数通过直接构造对象的方式避免了内存的拷贝和移动。

在这里插入图片描述

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/Long_xu/article/details/127383981

智能推荐

分布式光纤传感器的全球与中国市场2022-2028年:技术、参与者、趋势、市场规模及占有率研究报告_预计2026年中国分布式传感器市场规模有多大-程序员宅基地

文章浏览阅读3.2k次。本文研究全球与中国市场分布式光纤传感器的发展现状及未来发展趋势,分别从生产和消费的角度分析分布式光纤传感器的主要生产地区、主要消费地区以及主要的生产商。重点分析全球与中国市场的主要厂商产品特点、产品规格、不同规格产品的价格、产量、产值及全球和中国市场主要生产商的市场份额。主要生产商包括:FISO TechnologiesBrugg KabelSensor HighwayOmnisensAFL GlobalQinetiQ GroupLockheed MartinOSENSA Innovati_预计2026年中国分布式传感器市场规模有多大

07_08 常用组合逻辑电路结构——为IC设计的延时估计铺垫_基4布斯算法代码-程序员宅基地

文章浏览阅读1.1k次,点赞2次,收藏12次。常用组合逻辑电路结构——为IC设计的延时估计铺垫学习目的:估计模块间的delay,确保写的代码的timing 综合能给到多少HZ,以满足需求!_基4布斯算法代码

OpenAI Manager助手(基于SpringBoot和Vue)_chatgpt网页版-程序员宅基地

文章浏览阅读3.3k次,点赞3次,收藏5次。OpenAI Manager助手(基于SpringBoot和Vue)_chatgpt网页版

关于美国计算机奥赛USACO,你想知道的都在这_usaco可以多次提交吗-程序员宅基地

文章浏览阅读2.2k次。USACO自1992年举办,到目前为止已经举办了27届,目的是为了帮助美国信息学国家队选拔IOI的队员,目前逐渐发展为全球热门的线上赛事,成为美国大学申请条件下,含金量相当高的官方竞赛。USACO的比赛成绩可以助力计算机专业留学,越来越多的学生进入了康奈尔,麻省理工,普林斯顿,哈佛和耶鲁等大学,这些同学的共同点是他们都参加了美国计算机科学竞赛(USACO),并且取得过非常好的成绩。适合参赛人群USACO适合国内在读学生有意向申请美国大学的或者想锻炼自己编程能力的同学,高三学生也可以参加12月的第_usaco可以多次提交吗

MySQL存储过程和自定义函数_mysql自定义函数和存储过程-程序员宅基地

文章浏览阅读394次。1.1 存储程序1.2 创建存储过程1.3 创建自定义函数1.3.1 示例1.4 自定义函数和存储过程的区别1.5 变量的使用1.6 定义条件和处理程序1.6.1 定义条件1.6.1.1 示例1.6.2 定义处理程序1.6.2.1 示例1.7 光标的使用1.7.1 声明光标1.7.2 打开光标1.7.3 使用光标1.7.4 关闭光标1.8 流程控制的使用1.8.1 IF语句1.8.2 CASE语句1.8.3 LOOP语句1.8.4 LEAVE语句1.8.5 ITERATE语句1.8.6 REPEAT语句。_mysql自定义函数和存储过程

半导体基础知识与PN结_本征半导体电流为0-程序员宅基地

文章浏览阅读188次。半导体二极管——集成电路最小组成单元。_本征半导体电流为0

随便推点

【Unity3d Shader】水面和岩浆效果_unity 岩浆shader-程序员宅基地

文章浏览阅读2.8k次,点赞3次,收藏18次。游戏水面特效实现方式太多。咱们这边介绍的是一最简单的UV动画(无顶点位移),整个mesh由4个顶点构成。实现了水面效果(左图),不动代码稍微修改下参数和贴图可以实现岩浆效果(右图)。有要思路是1,uv按时间去做正弦波移动2,在1的基础上加个凹凸图混合uv3,在1、2的基础上加个水流方向4,加上对雾效的支持,如没必要请自行删除雾效代码(把包含fog的几行代码删除)S..._unity 岩浆shader

广义线性模型——Logistic回归模型(1)_广义线性回归模型-程序员宅基地

文章浏览阅读5k次。广义线性模型是线性模型的扩展,它通过连接函数建立响应变量的数学期望值与线性组合的预测变量之间的关系。广义线性模型拟合的形式为:其中g(μY)是条件均值的函数(称为连接函数)。另外,你可放松Y为正态分布的假设,改为Y 服从指数分布族中的一种分布即可。设定好连接函数和概率分布后,便可以通过最大似然估计的多次迭代推导出各参数值。在大部分情况下,线性模型就可以通过一系列连续型或类别型预测变量来预测正态分布的响应变量的工作。但是,有时候我们要进行非正态因变量的分析,例如:(1)类别型.._广义线性回归模型

HTML+CSS大作业 环境网页设计与实现(垃圾分类) web前端开发技术 web课程设计 网页规划与设计_垃圾分类网页设计目标怎么写-程序员宅基地

文章浏览阅读69次。环境保护、 保护地球、 校园环保、垃圾分类、绿色家园、等网站的设计与制作。 总结了一些学生网页制作的经验:一般的网页需要融入以下知识点:div+css布局、浮动、定位、高级css、表格、表单及验证、js轮播图、音频 视频 Flash的应用、ul li、下拉导航栏、鼠标划过效果等知识点,网页的风格主题也很全面:如爱好、风景、校园、美食、动漫、游戏、咖啡、音乐、家乡、电影、名人、商城以及个人主页等主题,学生、新手可参考下方页面的布局和设计和HTML源码(有用点赞△) 一套A+的网_垃圾分类网页设计目标怎么写

C# .Net 发布后,把dll全部放在一个文件夹中,让软件目录更整洁_.net dll 全局目录-程序员宅基地

文章浏览阅读614次,点赞7次,收藏11次。之前找到一个修改 exe 中 DLL地址 的方法, 不太好使,虽然能正确启动, 但无法改变 exe 的工作目录,这就影响了.Net 中很多获取 exe 执行目录来拼接的地址 ( 相对路径 ),比如 wwwroot 和 代码中相对目录还有一些复制到目录的普通文件 等等,它们的地址都会指向原来 exe 的目录, 而不是自定义的 “lib” 目录,根本原因就是没有修改 exe 的工作目录这次来搞一个启动程序,把 .net 的所有东西都放在一个文件夹,在文件夹同级的目录制作一个 exe._.net dll 全局目录

BRIEF特征点描述算法_breif description calculation 特征点-程序员宅基地

文章浏览阅读1.5k次。本文为转载,原博客地址:http://blog.csdn.net/hujingshuang/article/details/46910259简介 BRIEF是2010年的一篇名为《BRIEF:Binary Robust Independent Elementary Features》的文章中提出,BRIEF是对已检测到的特征点进行描述,它是一种二进制编码的描述子,摈弃了利用区域灰度..._breif description calculation 特征点

房屋租赁管理系统的设计和实现,SpringBoot计算机毕业设计论文_基于spring boot的房屋租赁系统论文-程序员宅基地

文章浏览阅读4.1k次,点赞21次,收藏79次。本文是《基于SpringBoot的房屋租赁管理系统》的配套原创说明文档,可以给应届毕业生提供格式撰写参考,也可以给开发类似系统的朋友们提供功能业务设计思路。_基于spring boot的房屋租赁系统论文

推荐文章

热门文章

相关标签