[C++] Explanation of new features of C++11

The first part of the new feature explanation~ 

Article directory

  • foreword
  • 1. More important new features
    • 1. Unified initialization list
    • 2 .decltype keyword
    • 3. Rvalue reference + move semantics
  • Summarize


foreword

Introduction to C++11 :
In 2003 , the C++ Standards Committee submitted a Technical Corrigendum ( TC1 for short ) , so that the name C++03 has replaced
C++98 is called the latest C++ standard name before C++11 . However, since C++03 (TC1) is mainly for the loopholes in the C++98 standard
The core part of the language is not changed, so people habitually call the merger of the two standards the C++98/03 standard.
From C++0x to C++11 , the C++ standard has been sharpened for 10 years, and the second real standard is late. Compared with C++98/03 , C++11 has brought a considerable number of changes, including about 140 new features, and the C++03 standard
About 600 defect fixes, which makes C++11 more like a new language conceived from C++98/03 . Comparison,
C++11 can be better used for system development and library development, the grammar is more generic and simplified, more stable and safer, not only more functional
It is powerful and can improve the development efficiency of programmers. It is also used a lot in the company's actual project development, so we want to be a
Focus on learning .
Note: Simpler ones such as auto will not be explained anymore

1. More important new features

1. Uniform list initialization

I believe everyone should be familiar with {} initialization, such as int a[] = {1,2,3,4}, but in c++11, everything can be initialized with {}, and the assignment symbol can also be omitted.

int main()
{
	int x1 = 1;
	int x2{ 55 };
	return 0;
}


Let's demonstrate the custom type again:

class Date
{
public:
	Date(int year, int month, int day)
		:_year(year)
		, _month(month)
		, _day(day)
	{
		cout << "Date(int year, int month, int day)" << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date d1(2023, 1, 9);
	Date d2{ 2024,5,1 };
	return 0;
}

 There is no problem with custom types. Next, let's look at list, because list and vector have different meanings:

Why do we say that the meaning is different, because the built-in type we just initialized with {} is to call the constructor, and the custom type is the same. Then the parameters in vector and list are variable, how is this supported? This is because c++11 has added the std::initializer_list class, let's take a look below:

 We can see that the type of curly braces is an initializer_list, let's see if this can be modified:

 We can see that the content pointed to by initializer_list cannot be modified, because initializer_list is stored in the constant area. So how does STL support initialization with initializer_list? In fact, it is also very simple, that is, to add a constructor that supports initialization with initializer_list, as shown in the following figure:

 Let's take a look at other initialization usages:

 The initialization of v3 is to construct an anonymous object with {} inside, and then call initializer_list to initialize.

2. decltype keyword

The keyword decltype declares the variable to be of the type specified by the expression.
Variables can be defined by the type of the expression, as shown below:
int main()
{
	const int x = 1;
	double y = 2.2;
	vector<decltype(x* y)> ret;
	return 0;
}

 The role of this keyword is so much that we will not demonstrate it again.

3. Rvalue references and move semantics

There is a reference syntax in the traditional C++ syntax, and the new rvalue reference syntax feature in C++11 , so from now on we
The references learned before are called lvalue references. Regardless of whether an lvalue reference or an rvalue reference, it is an alias for the object .
What is an lvalue? What is an lvalue reference?
An lvalue is an expression representing data ( such as a variable name or a dereferenced pointer ) , we can get its address + we can assign it
Values, lvalues ​​can appear on the left side of assignment symbols, and rvalues ​​cannot appear on the left side of assignment symbols . The left after the const modifier when defining
Value, you cannot assign a value to it, but you can take its address. An lvalue reference is a reference to an lvalue, aliasing the lvalue.
What is an rvalue? What is an rvalue reference?
An rvalue is also an expression representing data, such as: literal constant, expression return value, function return value ( this cannot be an lvalue reference
return ) and so on, rvalues ​​can appear on the right side of assignment symbols, but cannot appear on the left side of assignment symbols, rvalues ​​cannot
Get the address . An rvalue reference is a reference to an rvalue, aliasing the rvalue.
Let's use the code to see what are the common rvalues:
int main()
{
	// 10  一个常量
	//  x + y 一个表达式
	// fmin(x,y) 一个函数返回值
	return 0;
}

Let's first look at whether an lvalue reference can refer to an rvalue:

 What about expressions?

The same is not true, but we said that the return values ​​​​to the function are temporary variables with constants, so we can add const:

 That's right, our lvalue references can refer to both lvalues ​​and rvalues. Let's try it out with rvalue references:

int main()
{
	int&& a1 = 10;
	double x = 10, y = 20;
	double&& ret = x + y;
	return 0;
}

 Just now our lvalue reference can refer to both lvalue and rvalue. Let's see if rvalue reference can refer to lvalue?

 Obviously, rvalue references cannot refer to lvalues. Here we talk about a small detail: rvalue references can alias the lvalue after move:

What does move mean? move can change a value into a dying value, such as our a variable in the above figure, the declaration cycle of this variable is originally in the main function, but after move, the declaration cycle of a becomes 200 lines, which is The function of move, that is, it must be an rvalue after move.

Next, let's compare lvalue references and rvalue references, and then we will enter the learning of rvalue references + move semantics.

Lvalue references are compared to rvalue references :
Summary of lvalue references:
1. Lvalue references can only refer to lvalues, not rvalues.
2. But const lvalue references can refer to both lvalues ​​and rvalues.
Summary of rvalue references:
1. Rvalue references can only be rvalues, not lvalues.
2. But the rvalue reference can move the lvalue later.
Rvalue reference usage scenarios and meanings:

 First of all, we can see that lvalue references and rvalue references can constitute overloading. Let's call it to see:

void func(int& a)
{
	cout << "func(int& a)" << endl;
}
void func(int&& a)
{
	cout << "func(int&& a)" << endl;
}
int main()
{
	int x = 10, y = 20;
	func(x);
	func(x + y);
	return 0;
}

 You can see that the compiler can correctly identify lvalues ​​and rvalues. Let's use the string class to demonstrate:

 We can see that the string class in the library supports rvalues. Let's talk about the benefits of supporting rvalues ​​here:

Originally, the return value of s1+s2 will call the copy construction to construct an anonymous object, and then use the anonymous object to call the copy construction to ret (note that this is not an assignment, because ret is a new object, and the assignment is only for the already defined Object), so the resources consumed here are very large, and with the rvalue reference + move semantics, it becomes a direct exchange of the return value with ret, that is to say, ret directly gets the resource of the return value of s1+s2 . Let's try it with our own implementation of string:

namespace sxy
{
	class string
	{
	public:
		typedef char* iterator;
		iterator begin()
		{
			return _str;
		}
		iterator end()
		{
			return _str + _size;
		}
		string(const char* str = "")
			:_size(strlen(str))
			, _capacity(_size)
		{
			//cout << "string(char* str)" << endl;
			_str = new char[_capacity + 1];
			strcpy(_str, str);
		}
		// s1.swap(s2)
		void swap(string& s)
		{
			::swap(_str, s._str);
			::swap(_size, s._size);
			::swap(_capacity, s._capacity);
		}
		// 拷贝构造
		string(const string& s)
			:_str(nullptr)
		{
			cout << "string(const string& s) -- 深拷贝" << endl;
			string tmp(s._str);
			swap(tmp);
		}
		// 赋值重载
		string& operator=(const string& s)
		{
			cout << "string& operator=(string s) -- 深拷贝" << endl;
			string tmp(s);
			swap(tmp);
			return *this;
		}
		~string()
		{
			delete[] _str;
			_str = nullptr;
		}
		char& operator[](size_t pos)
		{
			assert(pos < _size);
			return _str[pos];
		}
		void reserve(size_t n)
		{
			if (n > _capacity)
			{
				char* tmp = new char[n + 1];
				strcpy(tmp, _str);
				delete[] _str;
				_str = tmp;
				_capacity = n;
			}
		}
		void push_back(char ch)
		{
			if (_size >= _capacity)
			{
				size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;
				reserve(newcapacity);
			}
			_str[_size] = ch;
			++_size;
			_str[_size] = '\0';
		}
		//string operator+=(char ch)
		string& operator+=(char ch)
		{
			push_back(ch);
			return *this;
		}
        string operator+(char ch)
		{
			string tmp(*this);
			tmp += ch;
			return tmp;
		}
		const char* c_str() const
		{
			return _str;
		}
	private:
		char* _str;
		size_t _size;
		size_t _capacity; // 不包含最后做标识的\0
	};
}
int main()
{
	sxy::string s1("hello world");
	sxy::string ret1 = s1;
	sxy::string ret2 = (s1 + '!');
	return 0;
}

The above is our own implementation of string, which does not implement the rvalue reference version:

 First construct a s1, then use s1 to copy and construct ret1, and call the copy construction here. s1+! is an rvalue. For the return value of the expression, the copy construction will be called once to generate an anonymous object, and then the copy construction will be called again to construct ret2 with this anonymous object. Below we add the rvalue reference version:

// 移动构造
		string(string&& s)
			:_str(nullptr)
			, _size(0)
			, _capacity(0)
		{
			cout << "string(string&& s) -- 移动构造" << endl;
			swap(s);
		}
		// 移动赋值
		string& operator=(string&& s)
		{
			cout << "string& operator=(string&& s) -- 移动赋值" << endl;
			swap(s);
			return *this;
		}

 Let's run it again:

 First, ret1 = s1 will call a copy construction, and after there is an rvalue reference, ret2 only needs to move the construction, but we use the copy construction when overloading operator +:

 That's why the following phenomenon occurs. Let's see how to transfer resources:

 We can see that the address of ret2 is 0xcccccc at the beginning, and then call the operator overloaded +. When entering the function, it is necessary to copy and construct a temporary object when returning tmp, but for the rvalue, the move construction is called here to directly exchange tmp and ret2. So in the end the address of ret2 directly becomes the address of tmp just now.

Let's look at something more obvious:

int main()
{
	sxy::string s1("hello world");
	sxy::string ret1 = s1;
	sxy::string ret2 = move(s1);
	return 0;
}

 It can be seen that s1 and ret2 have directly exchanged resources, so after move, a variable becomes a dying value. At this time, if we use the variable s1 again, it will be illegally accessed, so we must pay attention when using move. The previous value will become the dying value and cannot be used.

With so many cases listed above, let's summarize: lvalue references directly reduce copying, and lvalue references can be used to pass parameters, or they can be returned by reference, but lvalue references cannot solve the problem that local objects in functions cannot be returned by reference. And such problems need to be solved by rvalue references (such as Yanghui triangle, which returns a two-dimensional array of local objects, the cost of deep copying a two-dimensional array is too high, and rvalue references can be used to solve this very well. question). 

After C++11, all STL containers have added move construction, so we must use rvalues ​​​​in normal use.

Will resources be transferred in the following scenarios?

int main()
{
	sxy::string s1("hello world");
	move(s1);
	sxy::string ret2 = s1;
	return 0;
}

 Obviously not, move is actually a function call, this expression is an rvalue, access s1 alone, s1 is still an rvalue here to remember.

 After C++11, all STL container insertion data interface functions have added rvalue reference versions.

 For the insertion of the linked list, the ordinary insertion of s1 needs to copy and construct a hello world first, and then insert it into the linked list, and directly insert "hello hello" because this is an rvalue, so you can directly call the move construction and directly transfer the resources of this anonymous object transferred to the linked list.

 You can see the transfer of resources. Note: Anonymous objects are also rvalues

Let's summarize it again: lvalue references reduce copying and improve efficiency. Rvalue references also reduce copying and improve efficiency. But their perspectives are different. Lvalue references directly reduce copying. Rvalue references indirectly reduce copying, and identify whether it is an lvalue or an rvalue. If it is an rvalue, it will not directly move the copy directly to improve efficiency.

Let's take a look at the perfect forwarding:

First of all, let's explain: the rvalue reference in the template is a universal reference, which can receive both lvalue and rvalue.

void Fun(int& x) { cout << "左值引用" << endl; }
void Fun(const int& x) { cout << "const 左值引用" << endl; }
void Fun(int&& x) { cout << "右值引用" << endl; }
void Fun(const int&& x) { cout << "const 右值引用" << endl; }
template<typename T>
void PerfectForward(T&& t)
{
	Fun(t);
}
int main()
{
	PerfectForward(10);
	int a;
	PerfectForward(a);
	PerfectForward(std::move(a));
	const int b = 8;
	PerfectForward(b);
	PerfectForward(std::move(b));
	return 0;
}

 The following program can demonstrate the problem of perfect forwarding, let's run it first to see the result:

 All lvalue references, what's going on? (Note: When the parameter is passed, the next layer of the rvalue will become an lvalue.) First, 10 is an rvalue. After entering the PF function, the Fun function is called, and the rvalue enters the Fun function and becomes an lvalue, so regardless of the lvalue Or after the rvalue enters the Fun function, it becomes an lvalue, which is why all lvalues ​​are printed.

 So how to make it still be an rvalue when it enters fun? Use forward to forward it perfectly. Let's try it out:

 Now we have solved the problem just now, that is to say, when we use rvalue + move semantics, we must use perfect forwarding in order to keep the rvalue recursively.

Let's use our own linked list to demonstrate the problems that occur without perfect forwarding:

namespace sxy
{
	template<class T>
	struct list_node
	{
		list_node(const T& x = T())
			:_data(x)
			, _next(nullptr)
			, _prev(nullptr)
		{

		}
		list_node<T>* _prev;
		list_node<T>* _next;
		T _data;
	};
	template<class T, class Ref, class Ptr>
	struct list_iterator
	{
		typedef list_node<T> node;
		typedef list_iterator<T, Ref, Ptr> self;
		node* _node;
		list_iterator(node* n)
			:_node(n)
		{

		}
		Ref operator*()
		{
			return _node->_data;
		}
		self& operator++()
		{
			_node = _node->_next;
			return *this;
		}
		self operator++(int)
		{
			self tmp(*this);
			_node = _node->_next;
			return tmp;
		}
		self& operator--()
		{
			_node = _node->_prev;
			return *this;
		}
		self operator--(int)
		{
			self tmp(*this);
			_node = _node->_prev;
			return tmp;
		}
		Ptr operator->()
		{
			return &_node->_data;
		}
		bool operator!=(const self& it)
		{
			return _node != it._node;
		}
		bool operator==(const self& it)
		{
			return _node == it._node;
		}
	};
	template<class T>
	class list
	{
	public:
		typedef list_node<T> node;
		typedef list_iterator<T, T&, T*> iterator;
		typedef list_iterator<T, const T&, const T*> const_iterator;
		iterator begin()
		{
			return iterator(_head->_next);
		}
		iterator end()
		{
			return iterator(_head);
		}
		const_iterator begin() const
		{
			return const_iterator(_head->_next);
		}
		const_iterator end() const
		{
			return const_iterator(_head);
		}
		void empty_init()
		{
			_head = new node(T());
			_head->_next = _head;
			_head->_prev = _head;
		}
		list()
		{
			empty_init();
		}
		template<class Iterator>
		list(Iterator first, Iterator last)
		{
			empty_init();
			while (first != last)
			{
				push_back(*first);
				++first;
			}
		}
		list(const list<T>& ls)
		{
			empty_init();
			list<T> tmp(ls.begin(), ls.end());
			swap(tmp);
		}
		list<T>& operator=(list<T> ls)
		{
			swap(ls);
			return *this;
		}
		void swap(list<T>& ls)
		{
			std::swap(_head, ls._head);
		}
		~list()
		{
			clear();
			delete _head;
			_head = nullptr;
		}
		void push_back(const T& x)
		{
			insert(end(), x);
		}
		void push_back(T&& x)
		{
			insert(end(), forward<T>(x));
		}
		void push_front(const T& x)
		{
			insert(begin(), x);
		}
		void insert(iterator pos, const T& x)
		{
			node* cur = pos._node;
			node* prev = cur->_prev;
			node* newnode = new node(x);
			newnode->_next = cur;
			cur->_prev = newnode;
			newnode->_prev = prev;
			prev->_next = newnode;
		}
		iterator erase(iterator pos)
		{
			assert(pos != end());
			node* prev = pos._node->_prev;
			node* tail = pos._node->_next;
			prev->_next = tail;
			tail->_prev = prev;
			delete pos._node;
			return iterator(tail);
		}
		void pop_front()
		{
			erase(begin());
		}
		void pop_back()
		{
			erase(_head->_prev);
		}
		void clear()
		{
			iterator it = begin();
			while (it != end())
			{
				//it = erase(it);
				erase(it++);
			}
		}
	private:
		node* _head;
	};
}

 The above is the list source code implemented by ourselves, and the following is the test code:

int main()
{
	sxy::list<sxy::string> lt;
	sxy::string s1("hello world");
	lt.push_back(s1);
	lt.push_back("hello hello");
	return 0;
}

 We can see that the deep copy is used here because our own list does not implement the rvalue version. Now let's implement it:

First, when the linked list is inserted, it needs to judge whether it is an rvalue, so we first modify push_back:

void push_back(T&& x)
		{
			insert(end(), x);
		}

 After running, it does enter the rvalue version of push_back

 You can see that when you go down and enter the insert, you enter the lvalue version, then we add an rvalue version to the insert:

void insert(iterator pos, T&& x)
		{
			node* cur = pos._node;
			node* prev = cur->_prev;
			node* newnode = new node(x);
			newnode->_next = cur;
			cur->_prev = newnode;
			newnode->_prev = prev;
			prev->_next = newnode;
		}

 It can be seen that even if we implement the rvalue version, it still does not enter. This is the perfect forwarding problem we just talked about. The rvalue just entered enters the next layer and becomes an lvalue, so let’s forward it now:

 Now let's run it:

 This time successfully into the rvalue version:

However, when entering a new node, the constructor is still the construction of the lvalue version, so we add another rvalue version of the node construction:

list_node(T&& x = T())
			:_data(forward<T>(x))
			, _next(nullptr)
			, _prev(nullptr)
		{

		}

 This time we run it:

 This time we see success, and the above is the problem caused by perfect forwarding.

Here we summarize:

Both lvalue references and rvalue references alias objects to reduce copying. Lvalue references solve most scenarios. The following scenarios cannot be solved by lvalue references:

1. Partial object return problem.

2. Insert interface, object copy problem.

The rvalue reference + move semantics solve the above problems: 1. For shallow copy classes, move construction is equivalent to copy construction, because there is no transfer of resources.

2. The class of deep copy, here is the move structure. For the class of deep copy, the move structure can transfer the resource of rvalue (the value of x) without copying to improve efficiency.

Let's look at move assignment again, which is the same as move construction:

Here we assign the return value of the to_string function to s1. First, this function will call the move structure to get the resource of the return value in to_string, and then call the move assignment to directly exchange the resource of s1 with the resource that just returned the value. That is to say As a whole, the resources of the return value of s1 and to_string are directly exchanged. If there is no move semantics before, this code needs these steps: first, the return value of the to_string function calls a copy construction, and then assigns the copied anonymous object to s1 When the second copy construction is called (note: the copy construction is used when most assignment overloads are implemented).

The above is the whole content of rvalue reference + move semantics.


Summarize

The more difficult part of this article is rvalue references. It should be noted that: rvalue references have greatly improved the efficiency of C++, and lvalue + rvalue references have reduced a lot of copies. The focus of the next article is mainly on usability Variadic templates and lambda functions.

Guess you like

Origin blog.csdn.net/Sxy_wspsby/article/details/130955297