[C++] C++11 rvalue reference | New default member function | Variable parameter template | lambda expression

1. Rvalue references and move semantics

1.1 lvalue references and rvalue references

Before C++11, we only had the concept of reference and had no contact with the concept of so-called lvalue reference or rvalue reference. Starting from C++11, the concept of rvalue reference has been added, so now we will Quotes make a conceptual distinction. The references we talked about before are all lvalue references. For content related to lvalue references, you can take a look at the article C++ references written by the blogger before .

Whether it is an lvalue reference or an rvalue reference, it is essentially an alias for the object.

So, how to distinguish between lvalue references and rvalue references?

An lvalue is an expression that represents data (a variable name or a dereferenced pointer). We can get its address + we can assign a value to it . The lvalue can be on the left side of the equal sign, and the rvalue cannot be on the left side of the equal sign. On the left , const-modified variables cannot be assigned a value, but they can take addresses.

An rvalue is also an expression, such as: a literal, the return value of an expression, the return value of a function. An rvalue can appear on the right side of an assignment symbol, but cannot appear on the left side of an assignment symbol. An rvalue cannot take an address .

An lvalue reference is to alias an lvalue, and an rvalue reference is to alias an rvalue.

void Test1()
{
    
    
    //左值
    int a = 1;
    double x = 1.1, y = 2.2;
    int* pb = new int(10);
    const int c = 2;
    //左值引用
    int& ra = a;
    int*& rpb = pb;
    const int& rc = c;
    int& pvalue = *pb;
    //右值
    10;
    x + y;
    min(x, y);
    //右值引用
    int&& rr1 = 10;
    int&& rr2 = x + y;
    int&& rr3 = min(x, y);
}

An interesting phenomenon:

We know that rvalues ​​cannot be assigned, but look at the following code

void Test2()
{
     
     
    int x = 1, y = 2;
    int&& rr1 = x + y;
    cout << "x + y:" << x + y << endl;
    cout << "rr1:" << rr1 << endl;
    rr1 = 10;
    cout << "rr1" << rr1 << endl;
}

An rvalue x+ycan be assigned after being referenced by an rvalue, that is, it becomes an lvalue.

This is because after aliasing the rvalue, it will be stored in a specific location , and then the address of that location can be obtained, so the value stored at the sub-address can be changed. If you don't want it to be changeable, you can use const to modify the rvalue reference. Of course, this feature will not be used in actual applications, so this feature is not important.

1.2 Comparison of lvalue references and rvalue references

Summary of lvalue references:

  • An lvalue reference can only refer to an lvalue, not an rvalue.
  • A const lvalue reference can reference both an lvalue and an rvalue (this can be compared with the constancy of the temporary variable we mentioned before , that temporary variable is an rvalue)
void Test3()
{
    
    
    //左值引用只能引用左值,不能引用右值
    int a = 10;
    int& ra1 = a;
    //int& ra2 = 10;//右值引用不能引用左值,因此这行代码报错
  
    //const左值引用既能引用左值,也能引用右值
    const int& ra3 = 10;//引用右值
    const int& ra4 = a;//引用左值
}

Summary of rvalue references:

  • Rvalue references can only reference rvalues, not lvalues.
  • The move function can change an lvalue into an rvalue, so an rvalue reference can refer to the lvalue after move.
void Test4()
{
    
    
    //右值引用只能引用右值,不能引用左值
    int&& r1 = 10;
    int a = 10;
    //int&& r2 = a;//右值引用引用左值报错
    //Xcode报错内容:Rvalue reference to type 'int' cannot bind to lvalue of type 'int'(无法将左值绑定到右值引用)
  
    //右值引用能引用move之后的左值
    int&& r3 = std::move(a);
}

1.3 Usage scenarios and significance of rvalue references

Since lvalue references already existed before C++11, why do we need to add the concept of rvalue references? Isn't it "superfluous"?

First, let’s summarize the benefits of lvalue references

  • Make function parameters: It can reduce copying and improve efficiency. It can be used as output parameters.
  • Making return values: can reduce copying and improve efficiency

Although lvalue references have the above advantages, if you encounter the following situation:

//状况一
template<class T>
T func(const T& val)
{
    
    
    T ret;
    //...
    return ret;
}
//状况二
void Test5()
{
    
    
    zht::string str;
    str = zht::to_string(-1234);
}

At this time, ret and str will be destroyed after they go out of scope. If T is an object similar to string, a deep copy will be required, which will reduce efficiency.

Here we use the string we implemented ourselves to see the calling situation more clearly: some output information is added to this string

namespace  zht
{
    
    
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(const char* str = "") -- 构造函数" << endl;
        _str = new char[_capacity + 1];
        strcpy(_str, str);
    }
    // s1.swap(s2)
    void swap(string& s)
    {
    
    
        std::swap(_str, s._str);
        std::swap(_size, s._size);
        std::swap(_capacity, s._capacity);
    }
    // 拷贝构造
    string(const string& s)
        :_str(nullptr)
    {
    
    
        cout << "string(const string& s) -- 深拷贝" << endl;
        _str = new char[s._capacity + 1];
        strcpy(_str, s._str);
        _size = s._size;
        _capacity = s._capacity;
    }
    // 赋值重载
    string& operator=(const string& s)
    {
    
    
        cout << "string& operator=(string s) -- 深拷贝 " << endl;
        if (this != &s)
        {
    
    
            delete[] _str;
            _str = new char[s._capacity + 1];
            strcpy(_str, s._str);
            _size = s._size;
            _capacity = s._capacity;
        }
        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)
    {
    
    
        push_back(ch);
        return *this;
    }
    const char* c_str() const
    {
    
    
        return _str;
    }
private:
    char* _str;
    size_t _size;
    size_t _capacity; // 不包含最后做标识的\0
};
string to_string(int value)
{
    
    
    bool flag = true;
    if (value < 0)
    {
    
    
        flag = false;
        value = 0 - value;
    }
    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;
}
}

Running the code in case 2 above, you can see that a deep copy is performed during the process. The cost of deep copying here is very high, which is the shortcoming of lvalue references, so the concept of rvalue references was proposed .

image-20230719173255829

Use rvalue references and move semantics to solve the above problem:

Before that, let us clarify a concept: in C++11, rvalue references are classified into two types: pure rvalues ​​and checkmate values . Pure rvalues ​​refer to built-in type expressions. The dying value refers to the value of the expression of the custom type . The so-called dying value refers to the value that is about to end the life cycle . Generally speaking, anonymous objects, temporary objects, and custom type objects after move are all It's checkmate value.

Let us think about it. In the above scenario, after deep copying, the original local variable str will be destructed, which is equivalent to us constructing an identical variable first and then destructing the local variable. Then we might as well hand over the resources of this variable to another variable for management, which can improve efficiency.

So there is the interface of move construction : move construction is also a constructor, and its parameter is an rvalue reference of the type. In fact, it transfers the resource passed in the rvalue and avoids deep copying, so it is called move construction. It is to move other people's resources to construct.

Next, let’s implement the move construction and move assignment of the string above.

// 移动构造
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;
}

After adding these two functions to our own string implementation, if we run the program just now, we will find that the function call has changed: deep copy has become mobile copy.

image-20230719171811137

Next, let’s analyze why this happens:

Before adding move semantics:

image-20230719174042866

After adding move semantics:

image-20230719174355620

Note: There is a saying that rvalue references extend the life cycle of variables . In fact, this statement is inaccurate. It just transfers the resources of one variable to another variable. The life cycle of the variable itself does not change. . If it must be said this way, it can be understood as extending the life cycle of this resource (but resources do not have a life cycle).

After C++11, move construction and move copy interfaces have been added to STL containers.

image-20230720004207405

image-20230720004237237

1.4 In-depth usage scenario analysis of lvalue references and rvalue references

According to the above, we know that rvalues ​​can only refer to rvalues, but must rvalues ​​not be able to refer to lvalues?

In some scenarios, we may really need the rvalue to refer to the lvalue to achieve move semantics.

When an rvalue needs to refer to an lvalue, the lvalue can be converted into an rvalue through the move function . In C++11, the std::move() function is in the <utility> header file. The only function of this function is to coerce an lvalue into an rvalue .

image-20230720004423230

It can be seen that after using move to change s1 from an lvalue to an rvalue, the compiler recognizes s1 as a dying value when inserted again , matches the move construct and does not find it in lt.

According to the above example, we know that the list in the library supports move construction. We have simulated the implementation of the list before, so can we modify the previous list so that it can also support move semantics?

First, attach here the source code of the previously implemented list.

namespace zht
{
    
    
	template<class T>
	struct __list_node
	{
    
    
		__list_node* _prev;
		__list_node* _next;
		T _data;
		__list_node(const T& data = T())
			:_data(data)
			, _prev(nullptr)
			, _next(nullptr)
		{
    
    }
	};
	template<class T, class Ref, class Ptr>
	struct __list_iterator
	{
    
    
		typedef __list_node<T> node;
		typedef __list_iterator<T, Ref, Ptr> Self;
		node* _pnode;

		__list_iterator(node* p)
			:_pnode(p)
		{
    
    }
		Ptr operator->()
		{
    
    
			return &operator*();
		}
		Ref operator*()
		{
    
    
			return _pnode->_data;
		}
		Self& operator++()
		{
    
    
			_pnode = _pnode->_next;
			return *this;
		}
		Self operator++(int)
		{
    
    
			Self tmp(*this);
			_pnode = _pnode->_next;
			return tmp;
		}
		Self& operator--()
		{
    
    
			_pnode = _pnode->_prev;
			return *this;
		}
		Self operator--(int)
		{
    
    
			Self tmp(*this);
			_pnode = _pnode->_prev;
			return tmp;
		}
		bool operator!=(const Self& it)
		{
    
    
			return _pnode != it._pnode;
		}
		bool operator==(const Self& it)
		{
    
    
			return _pnode == it._pnode;
		}
	};

	template<class T>
	class list
	{
    
    
		typedef __list_node<T> node;
	public:
		typedef __list_iterator<T, T&, T*> iterator;
		typedef __list_iterator<T, const T&, const T*> const_iterator;
		iterator begin()
		{
    
    
			return _head->_next;
		}
		iterator end()
		{
    
    
			return _head;
		}
		const_iterator begin() const
		{
    
    
			return _head->_next;
		}
		const_iterator end() const
		{
    
    
			return _head;
		}
		void empty_initialize()
		{
    
    
			_head = new node(T());
			_head->_next = _head;
			_head->_prev = _head;
			_size = 0;
		}
		list()
		{
    
    
			empty_initialize();
		}
		list(size_t n, const T& val = T())
		{
    
    
			empty_initialize();
			for (size_t i = 0; i < n; ++i)
			{
    
    
				push_back(val);
			}
		}
		template<class InputIterator>
		list(InputIterator first, InputIterator last)
		{
    
    
			empty_initialize();
			while (first != last)
			{
    
    
				push_back(*first);
				++first;
			}
		}
		list(const list<T>& lt)//经典写法
		{
    
    
		    empty_initialize();
		    for (const auto& e : lt)
		    {
    
    
		        push_back(e);
		    }
		}
		void swap(list<T>& lt)
		{
    
    
			std::swap(_head, lt._head);
		}
		list<T>& operator=(list<T>& lt)
		{
    
    
			if (this != *lt)
			{
    
    
				clear();
				for (auto& e : lt)
				{
    
    
					push_back(e);
				}
			}
		}
		bool empty()
		{
    
    
			return _head->_next == _head;
		}
		void clear()
		{
    
    
			while (!empty())
			{
    
    
				erase(--end());
			}
		}
		~list()
		{
    
    
			clear();
			delete _head;
			_head = nullptr;
		}
		void push_back(const T& val = T())
		{
    
    
			insert(end(), val);
		}
		void push_front(const T& val = T())
		{
    
    
			insert(begin(), val);
		}
		iterator insert(iterator pos, const T& val = T())
		{
    
    
			node* newnode = new node(val);
			node* prev = pos._pnode->_prev;
			node* cur = pos._pnode;
			prev->_next = newnode;
			newnode->_prev = prev;
			newnode->_next = cur;
			cur->_prev = newnode;
			++_size;
			return iterator(newnode);
		}

		size_t size()
		{
    
    
			return _size;
		}
		iterator erase(iterator pos)
		{
    
    
			assert(pos != end());
			node* prev = pos._pnode->_prev;
			node* next = pos._pnode->_next;
			prev->_next = next;
			next->_prev = prev;
			delete pos._pnode;
			--_size;
			return iterator(next);
		}
		void pop_back()
		{
    
    
			erase(--end());
		}
		void pop_front()
		{
    
    
			erase(begin());
		}
		void resize(size_t n, const T& val = T())
		{
    
    
			while (n < size())
			{
    
    
				pop_back();
			}
			while (n > size())
			{
    
    
				push_back(val);
			}
		}
	private:
		node* _head;
		size_t _size;
	};
}

Using our own implemented list to execute Test6 in 1.4, we will get the following results:

image-20230720011329456

In order to make our list look like the list in the library, the first thing we consider is to implement the rvalue reference version of push_back. Since insert is called in push_back, insert also needs to add the rvalue reference version. The same is true in The rvalue reference version of the constructor also needs to be added to the construction of node, so the added functions are as follows:

//template<class T> struct __list_node
__list_node(T&& data)
    :_data(data)
    , _prev(nullptr)
    , _next(nullptr)
{
    
    }
//template<class T> class list
void push_back(T&& val)
{
    
    
    insert(end(), val);
}
iterator insert(iterator pos, T&& val)
{
    
    
    node* newnode = new node(val);
    node* prev = pos._pnode->_prev;
    node* cur = pos._pnode;
    prev->_next = newnode;
    newnode->_prev = prev;
    newnode->_next = cur;
    cur->_prev = newnode;
    ++_size;
    return iterator(newnode);
}

So now try to execute the following and find that there is no change. Why is this?

(You can practice it yourself here) Through debugging, you can find that the rvalue reference version of push_back is indeed called, but if you continue to debug below, you find that the lvalue version of insert is called. What is the reason? ?

Because the variable itself is an lvalue after an rvalue reference, the attribute of val here is an lvalue, so of course it will match the lvalue version of insert, so when passing parameters here, you need to change the attribute of this val into an rvalue. Here you can use move to change the attributes after passing the parameters, and then compile and run, and you will find that the results are still the same as the original ones. This is because the function is a function, and each layer needs to change the parameter attributes to rvalues, which is very troublesome. If we use move to modify all parameters:

__list_node(T&& data)
    :_data(move(data))
    , _prev(nullptr)
    , _next(nullptr)
{
    
    }		
void push_back(T&& val)
{
    
    
    insert(end(), move(val));
}
iterator insert(iterator pos, T&& val)
{
    
    
    node* newnode = new node(move(val));
    node* prev = pos._pnode->_prev;
    node* cur = pos._pnode;
    prev->_next = newnode;
    newnode->_prev = prev;
    newnode->_next = cur;
    cur->_prev = newnode;
    ++_size;
    return iterator(newnode);
}

Run the previous code again, and you can see that the effects in the library have been implemented. There is an additional string constructor and move constructor because they are called when initializing lt. Since the implementation of our string and list is still slightly different from that in the library, Ordered.

image-20230720144831650

At this point, we have completed the support for move semantics in our list.

1.5 Perfect forwarding

1.5.1 Universal reference

Above, we separately define a function whose parameters are rvalue references, and then let the compiler choose to call the construction/insertion interface whose parameters are lvalue references or the construction/insertion interface whose parameters are rvalue references based on the type of the actual parameters. So, can we enable functions to automatically instantiate different functions based on the types of actual parameters?

Universal references can achieve this function.

The so-called universal reference is actually a template, and the formal parameters of the function are rvalue references. For this kind of template, the compiler can automatically derive and instantiate different formal parameter types based on the type of the actual parameter. This template can receive lvalue/constlvalue/rvalue/constrvalue. The derived type is: lvalue reference/const lvalue reference/rvalue reference/const rvalue reference .

for example

template<class T>
void PerfectForward(T&& x)
{
    
    
    cout << "void PerfectForward(int&& x)" << endl;
}
void Test7()
{
    
    
    PerfectForward(10); // 右值
    int a = 10;
    PerfectForward(a); //左值
    const int b = 8;
    PerfectForward(b); //const 左值
    PerfectForward(std::move(b)); //const右值
}

If PerfectForward is called on four variables at the same time, all of them can be adjusted without error.

image-20230720154455772

image-20230720155149925

Let me mention here that if all four calls here appear, then x cannot be changed.

image-20230720155342785

❓But if the following two const calling statements are blocked, the compilation can be successful. What is the reason?

✅The PerfectForward we wrote is a function template . During the compilation and running process, four different functions will be instantiated. Since the const-modified formal parameters are instantiated during the call to pass parameters, this function after instantiation An error will be reported.

1.5.2 Perfect forwarding

Next we will make a little change to the above code

void Func(int& x)
{
    
    
    cout << "lvalue reference" << endl;
}
void Func(int&& x)
{
    
    
    cout << "rvalue reference " << endl;
}
void Func(const int& x)
{
    
    
    cout << "const lvalue reference " << endl;
}
void Func(const int&& x)
{
    
    
    cout << "const rvalue reference " << endl;
}
template<class T>
void PerfectForward(T&& x)
{
    
    
    Func(x);
}
void Test7()
{
    
    
    PerfectForward(10); // 右值
    int a = 10;
    PerfectForward(a); //左值
    const int b = 8;
    PerfectForward(b); //const 左值
    PerfectForward(std::move(b)); //const右值
}

image-20230720160515000

Running this code we find that no matter it is an lvalue reference or an rvalue reference, only the lvalue version of the Func function can be called. The reason has been mentioned above: after the rvalue reference, the variable itself is an lvalue , so it can only be called To the lvalue version of Func, how can we make the lvalue version call the lvalue version and the rvalue version call the rvalue version?

Use perfect forwarding std::forward. std::forward perfect forwarding retains the object's native type attributes during the parameter transfer process.

image-20230720160812989

2. New class functions

2.1 Default member function

In the previous article [C++] Classes and Objects, we mentioned that there are six default member functions of a class:

  • Constructor
  • destructor
  • copy constructor
  • Assignment overloaded function
  • Get address reload
  • const takes address overload

The first four are important, and the last two are of little use. However, two new default member functions have been added in C++11: move constructor and move assignment overloaded function . This is consistent with what we mentioned above. It corresponds.

The same two functions also have some difficult features:

1. Move constructor

Conditions automatically generated by the compiler:

  1. Did not implement the move construct by itself;
  2. Does not implement any of the destructor, copy construction, and copy assignment overloading

Characteristics of automatically generated move constructs:

  • For built-in types, they will be copied member by byte.
  • For a custom type, if the type member has a move constructor, the move constructor is called, otherwise the copy constructor is called.

2. Move assignment overloaded function

Conditions automatically generated by the compiler

  1. There is no own implementation of move assignment overloading
  2. Does not implement any of the destructor, copy construction, and copy assignment overloading

Features of automatically generated move assignment overloads:

  • For built-in types, they will be copied member by byte.
  • For a custom type, if the type member has a move assignment, the move assignment is called; otherwise, the move assignment is called.

Let's look at the following piece of code:

class Person
{
    
    
public:
    Person(const char* name = "", int age = 0)
        :_name(name)
        , _age(age)
    {
    
    }
private:
    zht::string _name;
    int _age;
};
void Test8()
{
    
    
    Person p1;
    Person p2 = p1;
    Person p3 = std::move(p1);
    Person p4;
    p4 = std::move(p2);
}

The running results are as follows:

image-20230721230445652

Analyze this result:

The first thing you can see is that the Person class does not implement move construction , nor does it implement any of the destructor, copy construction, and copy assignment overloading, so the compiler automatically generates move construction and other default constructors.

For the content of line 15, since p1 is an lvalue , it matches the copy construct . The copy construct generated by default in Person will call the copy construct in zht::string , so the output string(const string& s) -- 深拷贝, for line 16, after moving p1, becomes rvalue , so the move construct automatically generated by Person is called . This move construct will call the move construct in zht::string , so the output is that for line 18, p2move becomes an rvalue after p2move , and the move assignment automatically generated by Person is called . This function calls move assignment in zht::string , hence the output .string(string&& s) -- 移动构造string& operator=(string&& s) -- 移动赋值

2.2 Initialization of class member variables

C++11 allows initial default values ​​of member variables to be given when the class is defined. The default-generated constructor will be initialized using these default values ​​when initializing the list.

Look at the following piece of code:

class Date1
{
    
    
public:
    int _year;
    int _month;
    int _day;
};
class Date2
{
    
    
public:
    int _year = 1970;
    int _month = 1;
    int _day = 1;
};

void Test9()
{
    
    
    Date1 d11;
    Date2 d12;
}

image-20230722014627072

During the debugging process, we saw that the member variables in the object d11 corresponding to Date1 have not been initialized and are still with random values, but the object d12 corresponding to Date2 is initialized to the default value by default.

2.3 Force the keyword defaule to generate the default function

In 2.1 we mentioned that the default generation conditions for move construction and move assignment are relatively harsh. Assuming that we have written the copy constructor, the move constructor will not be generated, but if we want the move constructor to be automatically generated, we can use the default keyword to explicitly specify the move constructor 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)
    {
    
    }
    //这里使用default显示指定移动构造和移动赋值生成
    Person(Person&& p) = default;
    Person& operator=(Person&& p) = default;

    Person& operator=(const Person& p)
    {
    
    
        if(this != &p)
        {
    
    
            _name = p._name;
            _age = p._age;
        }
        return *this;
    }
    ~Person()
    {
    
    }
private:
    zht::string _name;
    int _age;
};
void Test8()
{
    
    
    Person p1;
    Person p2 = p1;
    Person p3 = std::move(p1);
    Person p4;
    p4 = std::move(p2);
}

image-20230722015921822

You can see that the copy construction and copy assignment overloads have been manually written at this time, but the move construction and move assignment overloads are still generated.

2.4 Disable keyword delete that generates default functions

If you want to restrict the generation or use of some default member functions, the approach in C++98 is to set the function to private and only generate it undefined , so that an error will be reported when called outside the class.

The approach in C++11 is even simpler: just add =delete to the declaration of the function . This syntax instructs the compiler not to generate a default version of the corresponding function. The function modified with =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:
    zht::string _name;
    int _age;
};
void Test8()
{
    
    
    Person p1;
    Person p2 = p1;
    Person p3 = std::move(p1);
    Person p4;
    p4 = std::move(p2);
}

image-20230722021202591

At this time, calling copy construction will result in an error:尝试引用已删除的函数

2.5 Final and override keywords in inheritance and polymorphism

Regarding the final and override keywords, they have been explained in previous blogs, so I won’t go into details here. Friends who need them can take a look.

【C++】Polymorphism

3. Variable parameter template

We learned about the concept of templates before, which allows classes and functions in our code to be templated to support multiple different types. But in C++98/03, the parameters of class templates and function templates can only be a fixed number, but in C++11, variable template parameters appear , allowing template parameters to receive different numbers of parameters.

Regarding variable parameter templates, here we only learn some basic features, and you only need to understand them. Friends who want to know more can search for information by themselves.

3.1 Syntax of variadic templates

For variable parameters, in fact, when we first started learning C language, we already used variable parameter functions. Yes, it is printfa function.

image-20230722023240796

Seeing that the printf function prototype ...represents variable parameters, C++11 also adopts a similar method. Let’s look at the following C++ variable parameter function template.

template<class ...Args>
void ShowList(Args... args)
{
    
    }

Note: Args here is a template parameter pack, args is a parameter pack of function parameters, this parameter pack can contain 0-N parameters

The above parameter args has an ellipsis in front of it , so it is a variable template parameter . We call the parameter with an ellipse a " parameter package ", which contains 0 to N (N>=0) template parameters.

There is only one way to get the number of parameters in the parameter pack: use the sizeof keyword

image-20230722025553608

There is one point to note here: we need to put the parameter package ...outside the brackets of sizeof . Don't think about the logic of this usage, just remember it as a new syntax.

But there is a problem here. We cannot directly obtain each parameter in the parameter package args. We can only obtain each parameter in the parameter package by expanding the parameter package. This is a main feature of using variable template parameters. It is also the biggest difficulty, that is, how to expand the variable template parameters. Since the syntax does not support using args[i] to obtain variable parameters , we use some strange tricks to obtain the value of the parameter package.

3.2 Recursive function method to expand parameter package

What we mainly use here is that the number of parameters in the parameter package can be any number , so we design an overloaded function as the exit of recursion.

//递归出口
template<class T>
void ShowList(const T& t)
{
    
    
    cout << t << endl;
}
//递归过程
template<class T, class ...Args>
void ShowList(T value, Args... args)
{
    
    
    cout << value << " ";
    ShowList(args...);//这里要使用...表示将参数包展开
}
void Test10()
{
    
    
    ShowList(1);
    ShowList(1, 'A');
    ShowList(1, 'A', string("sort"));
}

Here, if there is a parameter in the parameter package, the recursive exit will be called. If it is greater than one parameter, the recursive process will be called, and then the first parameter will be identified to T, and the remaining parameters will be put into the slave parameter package, recursively. transfer.

image-20230722032322286

3.3 Comma expression expansion parameter package

template<class T>
void PrintArgs(T t)
{
    
    
    cout << t << " ";
}
template<class... Args>
void ShowList(Args... args)
{
    
    
    int arr[] = {
    
     (PrintArgs(args), 0)... };
    cout << endl;
}
void Test10()
{
    
    
    ShowList(1);
    ShowList(1, 'A');
    ShowList(1, 'A', string("sort"));
}

This method of expanding the parameter package does not require a recursive termination function, but is expanded directly in the expand function body. PrintArg is not a recursive termination function, but a function that processes each parameter in the parameter package. The key to this in-place expansion of parameter packages 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) also follows this execution order. PrintArg(args) is executed first, and then the result of the comma expression is 0.

At the same time, another feature of C++11 is used - the initialization list. To initialize a variable-length array 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, during the process of creating the array, the printarg (args) part in front of the comma expression will be executed first to print out the parameters. That is to say, the parameter package will be expanded during the construction of the int array. The purpose of this array This is purely to expand the parameter pack during array construction.

3.4 Application of variable parameter templates in STL - empalce related interface functions

After adding the syntax of variable parameter templates, STL also added corresponding interfaces. Here we take a look at emplace in vector.

image-20230722033955948

First of all, the emplace series interfaces we saw support variable parameters of templates and are universally referenced. So what are the advantages of the insert and emplace series interfaces?

  • For built-in types, there is no difference in efficiency between the emplace interface and the traditional insertion interface, because built-in types are directly inserted and do not require copy construction;

  • For custom types that require deep copying, if the class implements the move constructor, the emplace interface will have one less shallow copy than the traditional insertion interface, but the overall efficiency is similar; if the class does not implement the move constructor, the emplace interface will The insertion efficiency is much higher than that of traditional insertion interfaces;

  • This is because in the traditional insertion interface, you need to create a temporary object first, and then deep copy or move the object into the container, while std::emplace() uses technologies such as variable parameter templates and universal templates. Construct objects directly in the container to avoid copying and moving objects;

  • For custom types that do not require deep copying, the emplace interface will require one less shallow copy (copy construction) than the traditional insertion interface, but the overall efficiency is similar; the reason is the same as above, the emplace interface can be constructed directly in situ in the container New objects avoid unnecessary copying process

In the previous article, we mentioned that the emplace interface is more efficient than the traditional insertion interface. If we can use emplace, we should not use the traditional insertion interface. In a strict sense, there is no problem with this statement, but it is not absolute; because in STL Containers all support move construction, so the emplace interface only eliminates a shallow copy, and the cost of shallow copy is not high; so we do not need to deliberately use the emplace series interfaces when using STL containers.

Note: The above move construction of the traditional interface and the direct construction of objects in the container of the emplace interface are only for rvalues ​​(dying values), and for lvalues, they can only perform deep copies honestly .

4. lambda expression

Before C++11, if we wanted to use sort, to use sort, we needed to pass a functor to specify the comparison principle.

image-20230722035630950

As you can see, if you want to sort built-in types, you can use the functor greater/less, but if you want to sort custom types, you have to write the functor yourself. Especially if the same class is sorted in different ways, it is necessary to implement different classes, especially the naming of the same class, which brings great inconvenience to developers. Therefore, Lambda was introduced in C++ 11 expression .

First, let’s take a look at what a lambda expression is

struct Goods
{
    
    
    string _name; // 名字
    double _price; // 价格
    int _evaluate; // 评价
    Goods(const char* str, double price, int evaluate)
        :_name(str)
        , _price(price)
        , _evaluate(evaluate)
    {
    
    }
};
void Test12()
{
    
    
    //这里想对存放的Goods对象按照不同的方式进行排序,就可以使用lambda表达式
    vector<Goods> v = {
    
     {
    
    "apple",2.1,5},{
    
    "banana",3,4},{
    
    "orange",2.2,3}, {
    
    "pineapple",1.5,4 } };
    sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) {
    
    
        return g1._name < g2._name; });
    cout << "sort by name" << endl;
    for (auto& e : v)
    {
    
    
        cout << e._name << " " << e._price << " " << e._evaluate << endl;
    }
    cout << endl;
    sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) {
    
    
        return g1._price < g2._price; });
    cout << "sort by price" << endl;
    for (const auto& e : v)
    {
    
    
        std::cout << e._name << " " << e._price << " " << e._evaluate << endl;
    }
    cout << endl;
    sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) {
    
    
        return g1._evaluate < g2._evaluate; });
    cout << "sort by evaluate" << endl;
    for (const auto& e : v)
    {
    
    
        cout << e._name << " " << e._price << " " << e._evaluate << endl;
    }
    cout << endl;
}

image-20230725210913074

4.1 Syntax and usage of Lambda expressions

lambda expression writing format

[capture-list] (parameters) mutable -> return-type { statement}

Expression description:

  • [capture-list]: Capture list . Appears at the beginning of the lambda function . The compiler uses [] to determine whether the following code is a lambda expression , so this item cannot be omitted . The capture list can capture the variables in the context for use in the lambda expression .

  • (parameters):parameter list. Consistent with the parameter list of an ordinary function, if parameter passing is not required, it can be omitted together with ().

  • mutable: By default, a lambda expression (function) is always a const function, and mutable can cancel its constness .

    Note: When using this modifier, the parameter list cannot be omitted

  • ->return-type:Return value type. Use the tracking return type form to declare the return value type of the function . This part can be omitted when there is no return value. It can also be omitted when the return value type is clear , and the compiler will deduce the return type.

  • {statement}: Function body. Within the function body, in addition to the function parameters in (parameters), you can also use the variables captured by [capture-list].

According to the above syntax format, we know that the parameter list and return value type are optional parts, and the capture list and function body can be empty

void Test13()
{
    
    
    // 最简单的lambda表达式,没有任何实际意义
    [] {
    
    };

    // 省略参数列表和返回值类型,返回值类型由编译器推导为int
    int a = 3, b = 4;
    [=] {
    
    return a + 3; };

    // 省略了返回值类型,无返回值类型
    auto fun1 = [&](int c) {
    
    b = a + c; };//由于lambda表达式的类型是编译器自动生成的,非常复杂,所有我们使用auto来定义
    fun1(10);
    cout << a << " " << b << endl;
    
    // 各部分都很完善的lambda函数
    auto fun2 = [=, &b](int c)->int {
    
    return b += a + c; };
    cout << fun2(10) << endl;

    // 赋值捕捉x
    int x = 10;
    auto add_x = [x](int a) mutable {
    
    
        x *= 2;
        return a + x; };
    cout << add_x(10) << endl;
}

Capture list description

The capture list describes which data in the context can be used by the lambda , and whether it is passed by value or by reference.

  • [var]: Indicates that the variable var is captured by passing by value.
  • [=]: Indicates that the value-passing method captures all variables of the parent scope (including this)
  • [&var]: Indicates a reference to the capture variable var
  • [&]: Indicates that the reference captures all variables of the parent scope (including this)
  • [this]: The way the flag value is passed captures the current this pointer

Notice

  1. The parent scope refers to the statement block containing the lambda function

  2. Syntactically, a capture list can consist of multiple capture items, separated by commas.

    For example: [=, &a, &b]: means that passing by reference captures a and b, and passing by value captures all other variables.

    [&, a, this]: Capture a and this in value transfer mode, and capture all other variables in reference mode.

  3. Capture lists do not allow variables to be passed repeatedly, otherwise it will cause compilation errors.

    We saw this usage in point 2. [=, &a]By default, all variables are captured by value transfer, but taking a out separately means that a will be processed separately and passed by reference. But if it is replaced here [=, a], repeated transmission will occur, which will lead to compilation errors.

  4. Lambda function capture list outside block scope must be empty

  5. A lambda function in a block scope can only capture local variables in the parent scope. Capturing any non-scope or non-local variables will result in a compilation error.

  6. Lambda expressions cannot be assigned to each other , even if they appear to be of the same type

image-20230726182710495

4.2 The underlying principles of Lambda expressions

In fact, the compiler processes lambda expressions at the bottom level by converting them into function objects (functors) and then processing them. operator()The so-called functor is an operator overloaded in a class . Let’s look at the following piece of code

class Add
{
    
    
public:
    Add(int base)
        :_base(base)
    {
    
    }
    int operator()(int num)
    {
    
    
        return _base + num;
    }
private:
    int _base;
};
void Test15()
{
    
    
    int base = 1;

    //仿函数的调用方式
    Add add1(base);//构造一个函数对象
    cout << add1(10) << endl;

    //lambda表达式
    auto add2 = [base](int num)->int 
    {
    
    
        return base + num; 
    };
    cout << add2(10) << endl;
}

An Add class is defined here, operator()overloaded in it, and then add1 is instantiated, which can be called a function object and can be used like a function. Then add2 is defined, and the lambda expression is assigned to add2. At this time, both add1 and add2 can be used like functions.

Next let’s take a look at the compilation situation

image-20230726205554268

Let's first look at 1 in assembly language. We can see that when using the function object, Add in Add is calledoperator() .

Look at 2. Here, lambda expressions are used to assign and call add2. You can see that the function is also called operator(). It is worth noting that what is called here is in <lambda_1> operator(). The essence is that the lambda expression is converted into a functor at the bottom .

❓Why can’t we explicitly write the type of lambda expression?

✅Because this type is automatically generated by the compiler, we have no way of knowing the specific way of writing this class name.

Under VS, the generated class name is called lambda_uuid, and the uuid in the class name is called Universally Unique Identifier. Simply put, uuid is a string generated through an algorithm to ensure that it can be used every time in the current program. The generated uuid will not be repeated, thus ensuring that the underlying class name of each lambda expression is unique.

(img-indD9wQt-1690376726287)]

4.2 The underlying principles of Lambda expressions

In fact, the compiler processes lambda expressions at the bottom level by converting them into function objects (functors) and then processing them. operator()The so-called functor is an operator overloaded in a class . Let’s look at the following piece of code

class Add
{
    
    
public:
    Add(int base)
        :_base(base)
    {
    
    }
    int operator()(int num)
    {
    
    
        return _base + num;
    }
private:
    int _base;
};
void Test15()
{
    
    
    int base = 1;

    //仿函数的调用方式
    Add add1(base);//构造一个函数对象
    cout << add1(10) << endl;

    //lambda表达式
    auto add2 = [base](int num)->int 
    {
    
    
        return base + num; 
    };
    cout << add2(10) << endl;
}

An Add class is defined here, operator()overloaded in it, and then add1 is instantiated, which can be called a function object and can be used like a function. Then add2 is defined, and the lambda expression is assigned to add2. At this time, both add1 and add2 can be used like functions.

Next let’s take a look at the compilation situation

[External link pictures are being transferred...(img-LEIRNSC7-1690376726288)]

Let's first look at 1 in assembly language. We can see that when using the function object, Add in Add is calledoperator() .

Look at 2. Here, lambda expressions are used to assign and call add2. You can see that the function is also called operator(). It is worth noting that what is called here is in <lambda_1> operator(). The essence is that the lambda expression is converted into a functor at the bottom .

❓Why can’t we explicitly write the type of lambda expression?

✅Because this type is automatically generated by the compiler, we have no way of knowing the specific way of writing this class name.

Under VS, the generated class name is called lambda_uuid, and the uuid in the class name is called Universally Unique Identifier. Simply put, uuid is a string generated through an algorithm to ensure that it can be used every time in the current program. The generated uuid will not be repeated, thus ensuring that the underlying class name of each lambda expression is unique.


End of this section…

Guess you like

Origin blog.csdn.net/weixin_63249832/article/details/131947742