Table of contents
2. Uniform list initialization
7. Rvalue references and move semantics
7.1 Lvalue and rvalue references
7.2 Comparison of lvalue references and rvalue references
7.3 rvalue reference usage scenarios and meaning
7.4 rvalue reference reference lvalue and some more in-depth usage scenarios analysis
1. Introduction to C++11:
In 2003, the C++ Standards Committee submitted a technical errata (TC1 for short), so that the name C++ 03 has replaced the name of the latest C++ standard before C++98 called C+++ 11.03 (TC1) Main is a bug fix in the C++98 standard, but the core part of the language has not been changed. Therefore, it is customary to merge the two standards into C++98/03.
From C++0x to C++11, the C++ standard has been sharpened in 10 years, and the second real standard has come late. compared to
C++98/03 and C++11 have brought considerable changes, including Bag, which contains about 140 new features, and corrections to about 600 defects in the C++03 standard, making C++11 More like a new language bred from C++98/03. In comparison, C++11 can be better used for system development and library development, the syntax is more generalized and simplified, stable and secure, not only functional, And can improve the efficiency of programmer development, the company's actual project development is also used more, so we want to learn as a focus. The syntax features added by C++11 are very extensive, and we can't explain them one by one here, so this section main learns the more practical syntax in practice.
Documentation link: C++11
short story:
1998 was the first year when the C++ Standards Committee was established. It was originally planned to update the standards every five years as needed. When the C++ International Standards Committee studied the next version of C++ 03, it originally planned to release it in 2007. So it was originally called C++ 07. But in 2006, the official felt that C++ 07 would not be completed in 2007, and the official felt that it might not be completed in 2008. Finally just called C++ 0x. The meaning of X is that I don't know if it can be completed in 2007, 2008 or 2009. As a result, it was not completed in 2010, and finally the C++ standard was finally completed in 2011. So it was finally named C++ 11.
2. Uniform list initialization
2.1 {} Initialization
In C++98, the standard allows uniform list initializers for array or struct elements using curly braces {}. for example:
struct Point { int _x; int _y; }; int main() { int array1[] = { 1, 2, 3, 4, 5 }; int array2[5] = { 0 }; Point p = { 1, 2 }; return 0; }
C++11 expands the use of curly bracketed lists (initialization lists) so that they can be used with all built-in and user-defined type s by adding an equal sign (=) or Can not be added when using initialization lists.
int main() { //List initialization (note: different from initialization list!) int x1 = 1; //General initialization int x2 = { 2 }; //{} initialize everything int x3{ 3 }; //{}initialization cout << "x1=" << x1 << endl; cout << "x2=" << x2 << endl; cout << "x3=" << x3 << endl; return 0; }
struct Point { int _x; int _y; }; int main() { int x1 = 1; int x2{ 2 }; int array1[]{ 1, 2, 3, 4, 5 }; int array2[5]{ 0 }; Point p{ 1, 2 }; // List initialization in C++11 can also be used in new expressions int* pa = new int[4]{ 0 }; return 0; }
struct Point { int _x; int _y; }; int main() { //List initialization (note: different from initialization list!) //remove the assignment number initialization int arr1[]{ 1,2,3,4 }; int arr2[5]{ 0 }; Point p{ 1,2 }; int* p1 = new int(1); int* p2 = new int[3]{ 1,3,4 }; return 0; }
You can also use list initialization to call the constructor initialization when you create an object:
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(2022, 1, 1); // old style // List initialization supported by C++11, where the constructor initialization will be called Date d2{ 2022, 1, 2 }; Date d3 = { 2022, 1, 3 }; return 0; }
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(2022, 1, 1); // general way // List initialization supported by C++11, where the constructor initialization will be called Date d2{ 2022, 1, 2 }; Date d3 = { 2022, 1, 3 }; Date* p4 = new Date(2022, 9, 18); Date* p5 = new Date[3]{ {2022, 9, 18},{2022,9,19},{2022,9,19} }; return 0; }
illustrate:
Although some of the new features look "unwieldy", they are foreshadowing for others (you may not use them, but you need to know this usage), and how the language is comfortable to use depends on the individual. The above new features, in one aspect, are to better support the initialization problem of new[ ].
2.2 std::initializer_list
Introductory documentation for std::initializer_list: https://cplusplus.com/reference/initializer_list/initializer_list/
int main() { // the type of il is an initializer_list auto il = { 10, 20, 30 }; cout << typeid(il).name() << endl; return 0; }
std::initializer_list usage scenarios:
std::initializer_list is generally used as a parameter of the constructor, and C++11 adds to many containers in STL
std::initializer_list takes the constructor as a parameter, so it is more convenient to initialize the container object. It can also be used as an argument to operator=, so that it can be assigned with curly braces.
Link:
https://cplusplus.com/reference/list/list/list/
https://cplusplus.com/reference/vector/vector/vector/
https://cplusplus.com/reference/map/map/map/
https://cplusplus.com/reference/vector/vector/operator=/
Make the mocked vector also support {} initialization and assignment:int main() { vector<int> v = { 1,2,3,4 }; list<int> lt = { 1,2 }; // Here {"sort", "sort"} will first initialize and construct a pair object map<string, string> dict = { {"sort", "sort"}, {"insert", "insert"} }; // Use curly brackets to assign values to containers v = {10, 20, 30}; return 0; }

//Mock the implemented vector to support {} initialization namespace my { template<class T> class vector { public: typedef T* iterator; vector(initializer_list<T> l) { _start = new T[l.size()]; _finish = _start + l.size(); _endofstorage = _start + l.size(); iterator vit = _start; typename initializer_list<T>::iterator lit = l.begin(); //iterator traversal /*while (lit != l.end()) { *vit++ = *lit++; }*/ //range for traversal for (auto e : l) { cout << e << " "; } cout << endl; } vector<T>& operator=(initializer_list<T> l) { vector<T> tmp(l); std::swap(_start, tmp._start); std::swap(_finish, tmp._finish); std::swap(_endofstorage, tmp._endofstorage); return *this; } private: iterator _start; iterator _finish; iterator _endofstorage; }; }
Note: Container support such as vector works on a different principle than Date class support!
For example Date class A:
{} initialization demo:
//4. List initialization of containers 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() { //General initialization vector<int> v1 (9); vector<int> v2(9,9); // List initialization (container initialization) supported by C++11 vector<int> v3 = { 1,2,3,4,5 }; vector<int> v4{ 6,7,8,9,10 }; vector<Date> v5{ {2022, 9, 18},{2022,9,19},{2022,9,19} }; return 0; }
//List initialization of other containers int main() { list<int> lt1(3); list<int> lt2(3,2); list<int> lt3{ 1,2,3 }; set<int> st1; set<int> st2{ 4,5,6 }; map<string, string> dict = { {"sort","sort"},{"left","left"},{"remaining","left"} }; return 0; }
The list of containers is initially supported by the principle: C++11 creates a new type:
3. Statement
c++11 offers several ways to simplify declarations, especially when using templates.
3.1 auto
In C++98 auto is a storage type specifier, indicating that the variable is a local automatic storage type, but the variable defined locally in the local domain is the automatic storage type by default, so auto has no value. In C++11, the original usage of auto is discarded, and it is used to implement automatic type leg breaking. This requires explicit initialization, which lets the compiler set the type of the defining object to the type of the initialization value.
int main() { int i = 10; auto p = &i; auto pf = strcpy; cout << typeid(p).name() << endl; cout << typeid(pf).name() << endl; map<string, string> dict = { {"sort", "sort"}, {"insert", "insert"} }; //map<string, string>::iterator it = dict.begin(); auto it = dict.begin(); return 0; }
3.2 decltype
The keyword decltype declares the type of the variable as the type specified by the expression.// Some usage scenarios of decltype template<class T1, class T2> void F(T1 t1, T2 t2) { decltype(t1 * t2) ret; cout << typeid(ret).name() << endl; } int main() { const int x = 1; double y = 2.2; decltype(x * y) ret; // The type of ret is double decltype(&x) p; // The type of p is int* cout << typeid(ret).name() << endl; cout << typeid(p).name() << endl; F(1, 'a'); return 0; }
3.3 nullptr
Since NULL is defined as a literal 0 in C++, this may bring back some problems, because 0 can represent both a pointer constant and an integer constant. So for the sake of clarity and safety, nullptr was added in C++11 to represent a null pointer.
#ifndef NULL #ifdef __cplusplus #define NULL 0 #else #define NULL ((void *)0) #endif #endif
4. Range for loop
It has been learned in the previous chapters, and for in-depth study, please refer to the official documentation.
Usage example:
5. Smart pointers
This part will be studied later.
6. Some changes in STL
new container
Circled in orange are a few new containers in C++11, but the ones that are actually most useful are unordered_map and
unordered_set.
Some new methods in containers
If we take a closer look, we will find that some C++11 methods are added to basically every container, but in fact, many of them are used less.
For example, the cbegin and cend methods are provided to return const iterators, etc., but they are of little practical significance, because begin and end can also return const iterators, which are icing on the cake.
In fact, after the C++11 update, the new method added in the container finally uses the rvalue reference version of the inserted interface function:
https://cplusplus.com/reference/vector/vector/emplace_back/
https://cplusplus.com/reference/map/map/emplace/
https://cplusplus.com/reference/map/map/insert/
https://cplusplus.com/reference/vector/vector/push_back/
But what exactly do these interfaces mean? It is said on the Internet that they can improve efficiency, how do they improve efficiency?
See the rvalue references and move semantics section below for an explanation. In addition, emplace also involves the variable parameters of the template, and you need to continue to study the knowledge in the following chapters.
7. Rvalue references and move semantics
7.1 Lvalue and rvalue references
There is a reference syntax in the traditional C++ syntax, and the rvalue reference syntax feature added in C++11, so from now on the reference we learned before is called lvalue reference. Both lvalue references and rvalue references are aliases to objects.
What is an lvalue? What is an lvalue reference?
An lvalue is an expression that represents data (such as a variable name or a dereferenced pointer), we can get its address + can assign a value to it, an lvalue can appear on the left side of an assignment symbol, and an rvalue cannot appear on the left side of an assignment symbol. When defining the lvalue after the const modifier, it cannot be assigned a value, but its address can be taken. An lvalue reference is a reference to an lvalue, aliasing an lvalue.
What is an rvalue? What is an rvalue reference?
An rvalue is also an expression that represents data, such as: literal constants, expression return values, function return values (this cannot be an lvalue reference return), etc. An rvalue can appear on the right side of an assignment symbol, but it cannot appear on the right side of an assignment symbol. On the left side of an assignment symbol, an rvalue cannot take an address. An rvalue reference is a reference to an rvalue, giving an alias to an rvalue.
It should be noted that the rvalue cannot take the address, but after the rvalue is aliased, the rvalue will be stored in a specific location, and the address of the location can be taken, that is to say, for example: the literal 10 cannot be taken. address, but after being referenced by rr1, you can take the address of rr1 or modify rr1. If you don't want rr1 to be modified, you can use const int&& rr1 to refer to it. Isn't it amazing? This is not the case to understand the actual use of rvalue references, and this feature is not important.
Test example:
7.2 Comparison of lvalue references and rvalue references
Summary of lvalue references:
1. An lvalue reference can only refer to an lvalue, not an rvalue.
2. But const lvalue reference can refer to both lvalue and rvalue
Summary of rvalue references:
1. An rvalue reference can only refer to an rvalue, not an lvalue.
2. But rvalue references can move later lvalues.
7.3 rvalue reference usage scenarios and meaning
Earlier we can see that lvalue references can refer to both lvalues and rvalues, so why does C++11 propose rvalue references? Is it superfluous? Let's take a look at the shortcomings of lvalue references, and how rvalue references make up for this shortcoming!
The problems encountered in the following scenarios can only be returned by value, and cannot be solved by using lvalue references:
Rvalue classification:
1. prvalue
2. will die
move can convert an element A with an lvalue attribute to an element A" with an rvalue attribute, which makes it possible to "plunder" the resources of the referenced element A" when others refer to the converted element A".
Problems with deep copy:
Return by value + deep copy:
Compiler optimization:There are both copy constructs and move constructs:
Compiler optimization:
Deep copy: lvalue copy will not be transferred by resources
rvalue copy: will transfer xvalue resources, but improve efficiency!
Move construction of STL containers:
The value is certain: After C++11, STL containers only eat move construction and move assignment! This greatly improves development efficiency!
E.g:
Move assignment:
namespace my { 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); } // copy construction string(const string& s) :_str(nullptr) { cout << "string(const string& s) -- deep copy" << endl; string tmp(s._str); swap(tmp); } // assignment overloading string& operator=(const string& s) { cout << "string& operator=(string s) -- deep copy" << endl; string tmp(s); swap(tmp); return *this; } // move construction string(string && s) :_str(nullptr) , _size(0) , _capacity(0) { cout << "string(string&& s) -- move semantics" << endl; swap(s); } // move assignment string& operator=(string && s) { cout << "string& operator=(string&& s) -- move semantics" << endl; swap(s); 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; } const char* c_str() const { return _str; } private: char* _str; size_t _size; size_t _capacity; // Does not include the final \0 for marking }; }
Use cases for lvalue references:
Doing parameters and doing return values can improve efficiency.void func1(my::string s) {} void func2(const my::string& s) {} int main() { my::string s1("hello world"); // In the calls of func1 and func2, we can see that lvalue references are used as parameters to reduce copying and improve the efficiency of usage scenarios and values. func1(s1); func2(s1); // string operator+=(char ch) returns a deep copy by value // string& operator+=(char ch) passing lvalue references without copying improves efficiency s1 += '!'; return 0; }
Shortcomings of lvalue references:
However, when the function return object is a local variable, it does not exist outside the function scope, so it cannot be returned by lvalue reference, but can only be returned by value. For example: As can be seen in the bit::string to_string(int value) function, only return by value can be used here, and return by value will result in at least one copy construction (if it is some older compilers, it may be two copy constructions) ).
//Mock the implemented string class namespace my { 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); } // copy construction string(const string& s) :_str(nullptr) { cout << "string(const string& s) -- deep copy" << endl; string tmp(s._str); swap(tmp); } // assignment overloading string& operator=(const string& s) { cout << "string& operator=(string s) -- deep copy" << endl; string tmp(s); swap(tmp); return *this; } // move construction string(string&& s) :_str(nullptr) , _size(0) , _capacity(0) { cout << "string(string&& s) -- move semantics" << endl; swap(s); } // move assignment string& operator=(string&& s) { cout << "string& operator=(string&& s) -- move semantics" << endl; swap(s); 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; } const char* c_str() const { return _str; } private: char* _str; size_t _size; size_t _capacity; // Does not include the final \0 for marking }; } //Mock implementation to_string (with my::string attached) namespace my { my::string to_string(int value) { bool flag = true; if (value < 0) { flag = false; value = 0 - value; } my::string str; while (value > 0) { int x = value % 10; value /= 10; str += ('0' + x); } if (flag == false) { str += '-'; } std::reverse(str.begin(), str.end()); return str; } } int main() { // It can be seen in the bit::string to_string(int value) function, here // Only return by value can be used, and return by value will result in at least one copy construction (or two copy constructions if it is an older compiler). my::string ret1 = my::to_string(1234); my::string ret2 = my::to_string(-1234); return 0; }
Rvalue references and move semantics solve the above problems:
Add move construction to bit::string. The essence of move construction is to steal the resources of the parameter rvalue. If the place is already occupied, then there is no need to make a deep copy, so it is called move construction, which is to steal other people's resources to construct itself .
Running the above two calls to bit::to_string again, we will find that the deep copy copy construction is not called here, but the move construction is called. There is no new open space in the move construction, and the data is copied, so the efficiency is improved.
Not just move construction, but move assignment:
Add a move assignment function to the bit::string class, and then call bit::to_string(1234), but this time it will be
The rvalue object returned by bit::to_string(1234) is assigned to the ret1 object, and the move construction is called at this time.
After this runs, we see that a move construction and a move assignment are called. Because if it is received with an existing object, the compiler can't optimize it. In the bit::to_string function, str is used to construct a temporary object, but we can see that the compiler is very smart to recognize str as an rvalue and call the move construction. Then assign this temporary object to ret1 as the return value of the bit::to_string function call, and the move assignment is called here.
Containers in the STL all add move construction and move assignment:
https://cplusplus.com/reference/string/string/string/
https://cplusplus.com/reference/vector/vector/vector/
7.4 rvalue reference reference lvalue and some more in-depth usage scenarios analysis
According to the grammar, rvalue references can only refer to rvalues, but rvalue references must not refer to lvalues? Because: In some cases, it may really be necessary to use rvalues to refer to lvalues To achieve move semantics. When you need to refer to an lvalue with an rvalue reference, you can convert the lvalue to an rvalue by using the move function. In C++ 11, std:: the move() function is in the header file, the function The name is deceptive, it does not move anything, its only function is to coerce an lvalue into an rvalue reference, and then implement move semantics.
Summarize:
Deep copy of lvalue reference---->copy construction/copy assignment
Deep copy of rvalue reference ----> move construction/move assignment
1. Deep copy the object, return by value, and call the move construction, so the efficiency is improved.
The STL container insertion interface function also adds an rvalue reference version:
https://cplusplus.com/reference/vector/vector/push_back/
https://cplusplus.com/reference/list/list/push_back/https://cplusplus.com/reference/list/list/push_back/
It should be noted that the "deep copy, resource transfer, etc." that can be printed here is the STL that calls its own simulation implementation, and cannot be viewed if the library is called.
Rvalue references enable move construction and move assignment, realize resource transfer, and improve efficiency. If rvalue references are not supported, deep copying of lvalue references requires both creation and destruction, which reduces efficiency. But be careful with the use of move operations for rvalue references. Rvalue references solve some of the problems with return-by-value.
7.5 Perfect Forwarding
&& Universal Reference in Templates
void Fun(int& x) { cout << "lvalue reference" << endl; } void Fun(const int& x) { cout << "const lvalue reference" << endl; } void Fun(int&& x) { cout << "rvalue reference" << endl; } void Fun(const int&& x) { cout << "const rvalue quote" << endl; } // The && in the template does not represent an rvalue reference, but a universal reference, which can receive both lvalues and rvalues. // The template's universal reference just provides the ability to receive both lvalue references and rvalue references, // But the only function of the reference type is to limit the received type, and it will degenerate into an lvalue in subsequent use. // We want to be able to maintain its lvalue or rvalue properties during the transfer process, we need to use the perfect forwarding we learn below template<typename T> void PerfectForward(T&& t) { Fun(t); } int main() { PerfectForward(10); // rvalue int a; PerfectForward(a); // lvalue PerfectForward(std::move(a)); // rvalue const int b = 8; PerfectForward(b); // const lvalue PerfectForward(std::move(b)); // const rvalue return 0; }
Here, the receiving object of the rvalue reference is an lvalue attribute and can take the address.
std::forward perfect forwarding preserves the object's native type attributes during parameter passing
void Fun(int& x) { cout << "lvalue reference" << endl; } void Fun(const int& x) { cout << "const lvalue reference" << endl; } void Fun(int&& x) { cout << "rvalue reference" << endl; } void Fun(const int&& x) { cout << "const rvalue reference" << endl; } // std::forward<T>(t) maintains the primitive type attribute of t during the parameter passing. template<typename T> void PerfectForward(T&& t) { Fun(std::forward<T>(t)); } int main() { PerfectForward(10); // rvalue int a; PerfectForward(a); // lvalue PerfectForward(std::move(a)); // rvalue const int b = 8; PerfectForward(b); // const lvalue PerfectForward(std::move(b)); // const rvalue return 0; }
Actual usage scenarios of perfect forwarding:
8. New Class Features
default member function
It turns out that in the C++ class, there are 6 default member functions:
1. Constructor
2. Destructor
3. Copy constructor
4. Copy assignment overloading
5. Take address overloading
6. const takes address overloading
In the end it's the first 4 that matter, the last two are of little use. The default member function is that we do not write the compiler will generate a default.
C++11 added two: move constructor and move assignment operator overloading.
There are a few things to note about move constructor and move assignment operator overloading:// The following code cannot be reflected in vs2013, and the above features can be demonstrated under vs2019. class Person { public: Person(const char* name = "", int age = 0) :_name(name) , _age(age) {} /*Person(const Person& p) * :_name(p._name) ,_age(p._age) {}*/ /*Person& operator=(const Person& p) { if(this != &p) { _name = p._name; _age = p._age; } return *this; }*/ /*~Person() {}*/ private: my::string _name; int _age; }; int main() { Person s1; Person s2 = s1; Person s3 = std::move(s1); Person s4; s4 = std::move(s2); return 0; }
class member variable initialization
C++11 allows initial default values to be given to member variables when a class is defined, and the default generated constructor will use these default values to initialize.
The keyword default to force the generation of a default function:
C++11 gives you more control over which default functions to use. Suppose you want to use some default function, but for some reason this function is not generated by default. For example, if we provide a copy construction, the move construction will not be generated, then we can use the default keyword to display the specified move construction generation.
class Person { public: Person(const char* name = "", int age = 0) :_name(name) , _age(age) {} Person(const Person& p) :_name(p._name) , _age(p._age) {} Person(Person&& p) = default; private: my::string _name; int _age; }; int main() { Person s1; Person s2 = s1; Person s3 = std::move(s1); return 0; }
The delete keyword that disables the generation of default functions:
If you want to restrict the generation of certain default functions, in C++98, the function is set to private, and only the patch is declared, so that if others want to call it, an error will be reported. It is simpler in C++11, just add =delete to the function declaration. This syntax instructs the compiler not to generate the default version of the corresponding function, and the function modified by =delete is called a delete function.
class Person { public: Person(const char* name = "", int age = 0) :_name(name) , _age(age) {} Person(const Person& p) = delete; private: my::string _name; int _age; }; int main() { Person s1; Person s2 = s1; Person s3 = std::move(s1); return 0; }
fifinal and override keywords in inheritance and polymorphism
This has been covered in the previous study. For in-depth study, please refer to the official documentation.
9. Variadic Templates
C++11's new feature variadic templates allows you to create function templates and class templates that can accept variadic parameters, compared to
In C++98/03, class templates and function templates can only contain a fixed number of template parameters. Variable template parameters are undoubtedly a huge improvement. However, because the variable template parameters are relatively abstract and require certain skills to use, this part is still relatively obscure. At this stage, it is enough for us to master some basic variable parameter template features, so we will stop here, and if you need it, you can study it in depth.
Here is a basic variadic function template:// Args is a template parameter pack, args is a function parameter pack // Declare a parameter pack Args...args, which can contain 0 to any number of template parameters. template <class ...Args> void ShowList(Args... args) {}
The parameter args above is preceded by an ellipsis, so it is a variable template parameter. We call the parameter with an ellipsis a "parameter package", which contains 0 to N (N>=0) template parameters. We cannot directly obtain each parameter in the parameter pack args. We can only obtain each parameter in the parameter pack by expanding the parameter pack. This is a main feature of using variable template parameters, and it is also the biggest difficulty, which How to expand variable template parameters. Since the syntax does not support the use of args[i] to obtain variable parameters, we use some strange tricks to obtain the value of the parameter pack one by one.
// Args is a template parameter pack, args is a function parameter pack // Declare a parameter pack Args...args, which can contain 0 to any number of template parameters. template <class ...Args> void ShowList(Args... args) { cout << sizeof...(args) << endl; } int main() { ShowList(1, 'x', 1.1); ShowList(1, 2, 3, 4, 5); return 0; }
Expand the parameter pack in a recursive function way
// Args is a template parameter pack, args is a function parameter pack // Declare a parameter pack Args...args, which can contain 0 to any number of template parameters. template <class T> void ShowList(const T& val) { cout << val <<"type:" << typeid(val).name() << endl; } template <class T,class ...Args> void ShowList(const T& val,Args... args) { //Number of parameters cout <<"Number of parameters:" << sizeof...(args) << endl; cout << val << "type:" << typeid(val).name() << endl; ShowList(args...); } int main() { ShowList(1, 'x', 1.1); ShowList(1, 2, 3, 4, 5); return 0; }
// Args is a template parameter pack, args is a function parameter pack // Declare a parameter pack Args...args, which can contain 0 to any number of template parameters. template <class T> void ShowList(const T& val) { cout << val << "->" << typeid(val).name() << endl; } template <class T,class ...Args> void ShowList(const T& val,Args... args) { //Number of parameters cout << sizeof...(args) << endl; cout << val <<"->" << typeid(val).name() << endl; ShowList(args...); } int main() { ShowList(1, 'x', 1.1); //ShowList(1, 2, 3, 4, 5); return 0; }
// Args is a template parameter pack, args is a function parameter pack // Declare a parameter pack Args...args, which can contain 0 to any number of template parameters. void ShowList() {} template <class T, class ...Args> void ShowList(const T& val, Args... args) { //Number of parameters cout << sizeof...(args) << endl; cout << val << "->" << typeid(val).name() << endl; ShowList(args...); } int main() { ShowList(1, 'x', 1.1); //ShowList(1, 2, 3, 4, 5); return 0; }
Comma expression expands parameter pack
This way of expanding the parameter pack does not need to terminate the function recursively, and is directly expanded in the expand function body. printarg is not a recursive termination function, but a function that processes each parameter in the parameter pack. The key to this in-place expansion of parameter packs is the comma expression. We know that comma expressions will execute the expressions preceding the comma in order.
The comma expression in the expand function: (printarg(args), 0), which is also executed in this order of execution, first
printarg(args), and get the result 0 of the comma expression. At the same time, another feature of C++11 - initialization list is used, a variable-length array is initialized through the initialization list, {(printarg(args ), 0)...} will be expanded into ((printarg(arg1) ,0),(printarg(arg2),0), (printarg(arg3),0), etc... ), will eventually create an array int arr[sizeof...(Args)] whose element values are all 0 . Since it is a comma expression, in the process of creating an array, the part printarg ( args) in front of the comma expression will be executed first to print out the parameters, that is to say, the parameter pack will be expanded during the process of constructing the int array. The purpose of this array is purely for ArrayConstruct The procedure Expand parameter pack
template <class T> void PrintArg(T t) { cout << t << " "; } //expand function template <class ...Args> void ShowList(Args... args) { int arr[] = { (PrintArg(args), 0)... }; cout << endl; } int main() { ShowList(1); ShowList(1, 'A'); ShowList(1, 'A', std::string("sort")); return 0; }
empalce related interface functions in the STL container:
https://cplusplus.com/reference/vector/vector/emplace_back/
https://cplusplus.com/reference/list/list/emplace_back/
template <class... Args> void emplace_back (Args&&... args);
First of all, the emplace series of interfaces we saw supports variable parameters of templates and universal references. So what are the advantages over the insert and emplace series of interfaces?
int main() { std::list< std::pair<int, char> > mylist; // emplace_back supports variable parameters. After getting the parameters for constructing the pair object, you can create the object yourself // So here we can see that there is not much difference from push_back except for usage mylist.emplace_back(10, 'a'); mylist.emplace_back(20, 'b'); mylist.emplace_back(make_pair(30, 'c')); mylist.push_back(make_pair(40, 'd')); mylist.push_back({ 50, 'e' }); for (auto e : mylist) cout << e.first << ":" << e.second << endl; return 0; }
int main() { // Let's try bit::string with copy construction and move construction, and try again // We will find that there is no difference, emplace_back is constructed directly, push_back // It's better to construct first, then move the construct. std::list< std::pair<int, my::string> > mylist; mylist.emplace_back(10, "sort"); mylist.emplace_back(make_pair(20, "sort")); mylist.push_back(make_pair(30, "sort")); mylist.push_back({ 40, "sort" }); return 0; }