C++ | C++11 new features (Part 2)

Preface

        Earlier we introduced new features such as C++11 list initialization, new class functions, and rvalue references. This article continues to introduce new syntax such as variable parameter templates and lambda expressions;

1. Variable parameter template

        Before C++11, we had a common fixed number of template parameters, but we had no idea how to start with variable parameters. After C++11, variable template parameters were introduced; the formal parameters can receive 0 or more different parameters. Or parameters of the same type; the following is the specific code;

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

        Among them, Args is the variable parameter type name, and it can also be named by another name. We call args a parameter package, and it can also be named by another name; this parameter package can receive 0 or even more parameters; about this parameter package For analysis, we can use the following two methods;

1. Two ways to unpack 

// 方法一:递归取参
void showList()
{
	cout << endl;
}
template<class T, class ...Args>
void showList(const T& t, Args... args)
{
	cout << t << " ";
	showList(args...);
}

void test13()
{
	showList();
	showList(1);
	showList(1, 'x');
	showList(1, 1.11, 'y');
}
// 方式二:利用数组自动推导元素个数
template <class T>
void PrintArg(T t)
{
	cout << t << " ";
}
template <class ...Args>
void ShowList(Args... args)
{
    // 逗号表达式
	int arr[] = { 0, (PrintArg(args), 0)...};
	cout << endl;
}
void test13()
{
	ShowList();
	ShowList(1);
	ShowList(1, 'x');
	ShowList(1, 1.11, 'y');
}

2. emplace series interface

        The emplace series interfaces are all plug-in interfaces; they use parameter packages to transfer parameters; most containers in STL provide this plug-in interface; for example, the following;

        So what is the difference between the emplace insertion interface and the ordinary insertion interface? Here I use vector as an example;

void test14()
{
	list<MySpace::string> l1;
	MySpace::string str1("hello");
	// 无区别
	l1.push_back(str1);    // 深拷贝
	l1.emplace_back(str1); // 深拷贝

	cout << endl << endl;
	// 无区别
	l1.push_back(move(str1)); // 移动构造
	l1.emplace_back(move(str1)); // 移动构造

	cout << endl << endl;
	// 有区别
	l1.push_back("11111");    // 拷贝构造 + 移动构造
	l1.emplace_back("11111"); // 直接构造 
}

2. lambda

1. Initial lambda

        We often write code similar to the following;

struct Goods
{
	string _name; // 名字
	double _price; // 价格
	int _evaluate; // 评价
	Goods(const char* str, double price, int evaluate)
		:_name(str)
		, _price(price)
		, _evaluate(evaluate)
	{}
};

struct CmpByPriceLess
{
	bool operator()(const Goods& x, const Goods& y)
	{
		return x._price < y._price;
	}
};

struct CmpByPriceGreater
{
	bool operator()(const Goods& x, const Goods& y)
	{
		return x._price > y._price;
	}
};

void test1()
{
	vector<Goods> v = { { "苹果", 2.1, 5 }, { "香蕉", 3, 4 }, { "橙子", 2.2,
3 }, { "菠萝", 1.5, 4 } };
	// 价格升序
	sort(v.begin(), v.end(), CmpByPriceLess());
	// 价格降序
	sort(v.begin(), v.end(), CmpByPriceGreater());

}

        When using sort, we need to use the incoming functor; are you troubled by what name to pass in to this functor? In C++11, a new method is introduced, which is our lambda;

2. Use of lambda

The specific format of lambda is as follows;

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

You may still be a little confused after reading the above, let’s use it roughly;

void test1()
{
	vector<Goods> v = { { "苹果", 2.1, 5 }, { "香蕉", 3, 4 }, { "橙子", 2.2,
3 }, { "菠萝", 1.5, 4 } };
	// 价格升序
	//sort(v.begin(), v.end(), CmpByPriceLess());
	// 价格降序
	//sort(v.begin(), v.end(), CmpByPriceGreater());
	
	// 价格升序
	sort(v.begin(), v.end(), [](const Goods& x, const Goods& y)mutable->bool 
		{ 
			return x._price < x._price; 
		});
	// 价格降序
	sort(v.begin(), v.end(), [](const Goods& x, const Goods& y)mutable->bool 
		{ 
			return x._price > x._price; 
		});
}

        First, we introduce that what is in square brackets is our capture list , which can capture variables in the context for use by the lambda function; there are several capture methods:

[var]: value capture; at this time we are just passing the value, and modifying the value within the lambda function will not affect the captured value outside the lambda; by default, the values ​​we capture have const attributes, and our key to adding muscle is mutable Words can make var modifiable (without changing the captured value outside the lambda);

[ = ]: All values ​​are passed, all variables above are captured, and the values ​​are passed to the lambda function;

void test2()
{
	int a = 3;
	int b = 5; 
	// 值捕捉
	auto add = [a, b]()mutable->int 
	{ 
		a++;
		return a + b; 
	};
	// 全部值捕捉(若无参数,括号可省略;一般情况下,返回值也可以省略)
	// 如果不期望修改值传递进来的值,mutable也可以省略;
	auto sub = [=] { return a - b; };

	cout << add() << endl;
	cout << sub() << endl;
	cout << a << endl;

}

[ var& ]: Pass by reference, pass a value to the lambda function by reference; when we modify the value within the lambda function, since it is passed by reference, it will also affect the value itself;

[ & ]: Capture all the variables above by reference;

void test3()
{
	int x = 3;
	int y = 4;
	// 若添加了mutable则圆括号不可省略
	auto f1 = [&x, y]()mutable {x = 1; y = 2; };
	f1();
	cout << x << " " << y << endl;
	// 将上文所有变量以引用的方式进行捕捉
	auto f2 = [&] {x = 10; y = 20; };
	cout << x << " " << y << endl;
}

[ this ]: captures the this pointer of the current class; in fact, the previous = will also capture the this pointer within the class, but it is passed by value; while & is passed by reference;

class A
{
public:
	void func()
	{
		// 捕获当前类的this指针
		auto f1 = [this] {cout << _a << endl; };
	}
private:
	int _a = 0;
};

        Let’s look at the parentheses again. In fact, parentheses are similar to the parentheses in functions and are used to receive parameters; mutable has been introduced before. For ordinary value transfer, const modification is usually added. After adding mutable, Value transfer is not modified with const; after the arrow is the return value, which is generally dispensable; the curly braces are the body of our function;

3. The underlying principle of lambda

        We looked at the use of lambda earlier, so what is the underlying principle of lambda? Before that, we answer a question, can similar lambdas assign values ​​to each other? The following code;

// 是否正确?
void test3()
{
	auto f1 = [](int x, int y) {return x + y; };
	//auto f2 = [](int x, int y) {return x + y; };
	auto f2 = [](int x, int y) {return x - y; };

	f1 = f2;

}

        In fact, no matter whether we are similar or exactly the same, we cannot assign values. Why is this? Doesn't it look exactly the same? And what is the nature of our lambda function? I use the following piece of code to explain these issues;

struct Fun
{
	int operator()(int x, int y)
	{
		return x + y;
	}
};

void test4()
{
	auto f1 = [](int x, int y) { return x + y; };
	Fun f2;
	// 调用lambda函数
	f1(2, 5);
	// 调用仿函数
	f2(2, 5);

}

        We see the result after the code is converted to assembly;

        We found that the results of the two pieces of code after being converted to assembly are almost the same; yes, our lambda is actually a functor, but the compiler will automatically generate the type for us. The class name is lambda_ + UUID; UUID is a unique identifier; So although we look like the same lambda function, in fact, they are functors with different class names. Since the types are different, how can we assign values ​​to each other? Isn’t the previous problem easily solved?

3. Packaging

1. Initial function

        Our wrapper, also called an adapter, mainly wraps callable objects. What are the callable objects in C++? There are the function pointers we learned in C language, the functors we learned earlier, and the lambda functions we just learned; we can encapsulate them in the same way; as shown in the following code;

struct ADD
{
	int operator()(int x, int y)
	{
		return x + y;
	}
};

int add(int x, int y)
{
	return x + y;
}
void test5()
{
	// 包装器封装仿函数
	function<int(int, int)> f1 = ADD();
	// 包装器封装函数指针
	int(*pf)(int, int) = add;
	function<int(int, int)> f1 = pf;
	// 包装器封装lambda函数
	function<int(int, int)> f1 = [](int x, int y) { return x + y; };
}

        The wrapper can encapsulate some of the above regular functions, and can also wrap the member functions in our class; as shown below;

class A
{
public:
	// 静态成员函数
	static int func1(int x, int y)
	{
		return x + y;
	}
	// 普通成员函数
	int func2(int x, int y)
	{
		return x + y;
	}
private:
	
};
void test6()
{
	// 封装静态成员函数(类名前可加&也可不加)
	//function<int(int, int)> f1 = A::func1;
	function<int(int, int)> f1 = &A::func1;

	// 封装普通成员函数(类型前必须加&,语法规定,且参数列表中声明类名)
	function<int(A, int, int)> f2 = &A::func2;
}

2. Usage scenarios of wrappers

        Wrappers are mainly used in some scenarios where functions need to be processed uniformly. For example, we have a vector, which stores callable objects with some return values ​​and the same formal parameters; but these callable objects may be functors, or Function pointers may also be lambda functions. In this case, they are not of the same class. We can only encapsulate them into one class through functors;

struct functor
{
	int operator()(int x, int y)
	{
		return x - y;
	}
};

int mul(int x, int y)
{
	return x * y;
}

void test7()
{
	vector<function<int(int x, int y)>> vfs;
	// lambda函数
	vfs.push_back([](int x, int y)-> int {return x + y; });
	// 仿函数
	functor ft;
	vfs.push_back(ft);
	// 函数指针
	int(*ptr)(int, int) = mul;
	vfs.push_back(ptr);

}

3、bind

        bind is defined in the header file functional. It is more like an adapter for a function template, and the function above is an adapter for a class template. bind has two main functions, namely adjusting the order of parameters and adjusting the number of parameters. Let’s proceed one by one below . exhibit;

// 调整参数顺序
void Func1(int x, int y)
{
	cout << x << " " << y << endl;
}
void test8()
{
	// 调整参数顺序
	Func1(10, 20);
	// placeholder为命名空间,_1,_2....._n都为调整的参数顺序
	auto f1 = bind(Func1, placeholders::_2, placeholders::_1);
	// 写法二
	function<void(int, int)> f2 = bind(Func1, placeholders::_2, placeholders::_1);
	f1(10, 20);
}
// 调整参数个数
class Cal
{
public:
	Cal(double rate = 2.5)
		:_rate(rate)
	{}
	double cal(int x, int y)
	{
		return (x + y) * _rate;
	}
private:
	double _rate;
};
void test9()
{
	int x = 3;
	int y = 6;
	Cal c1;
	cout << c1.cal(x, y) << endl;
	// 调整参数个数
	auto func2 = bind(&Cal::cal, c1, placeholders::_1, placeholders::_2);
	cout << func2(x, y) << endl;
}

        In fact, adjusting the number of parameters is to pass in the parameter displayed in our bind;

4. Thread library

        After C++11, an object-oriented thread operation method was introduced; compared with the past, it not only has the benefit of object orientation, but also solves cross-platform problems; our thread operation interface under Linux is the same as The thread operation interface under our window is different, so before C++11, we had cross-platform capabilities for programs with thread-related operations, and we had to implement two sets of implementation solutions through conditional compilation; but after C++11, We can achieve cross-platform by using our thread library (actually the bottom layer is still conditional compilation, but it has been written by others);

1. Thread library related interfaces

        The thread library provides us with the following related interfaces;

        If you have experience in multi-threaded development under Linux or Windows, you may be able to use the above interface at a glance; first, let’s look at the constructor;

        We don’t need to care about the destructor, it will be called automatically; our assignment overload only has the rvalue version, and the lvalue version has been deleted;

        Some other interfaces are also very simple to use. They are all public member functions without parameters. The specific functions are as shown in the figure below;

2. Some details about the thread library 

        Detail 1: The thread id returned by get_id here is not an integer like under Linux, but a structured data; the details are as follows;

// vs下查看
typedef struct
{ 
    /* thread identifier for Win32 */
    void *_Hnd; /* Win32 HANDLE */
    unsigned int _Id;
} _Thrd_imp_t;

        Detail 2: The first parameter when creating a thread can be a function pointer, a functor, or a lambda function;

void Func1()
{
	cout << "thread 1" << endl;
}

struct Func2
{
	void operator()()
	{
		cout << "thread 2" << endl;
	}
};


int main()
{
	// 函数指针
	thread t1(Func1);
	// 传仿函数对象
	Func2 f2;
	thread t2(f2);
	// 传lambda函数
	thread t3([] { cout << "thread 3" << endl; });

	t1.join();
	t2.join();
	t3.join();
	return 0;
}

        Detail 3 : Regarding the parameter passing of the thread function, if the thread function wants to pass a reference, it must call the ref function; and when we pass the reference and pointer, if we call detach to separate the thread, we need to pay attention to the reference/ If the object pointed to by the pointer is an object on the stack, out-of-bounds access may occur; that is, if the thread in which the object is located ends or goes out of the scope of the object, the object will be destroyed, and continued access by the child thread will cause out-of-bounds access. The phenomenon; 

3. Thread-related interfaces

        In addition to the member functions of the thread class, there are also the following functions from the this_thread namespace;

4. Issues related to thread safety

        Similarly, there will be thread safety issues when using our C++11 encapsulated thread library; the following code is a thread-safe code;

static int sum = 0;


void transaction(int num)
{
	for (int i = 0; i < num; i++)
	{
		sum++;
	}
}


int main()
{
	// 创建线程
	//template <class Fn, class... Args>
	//explicit thread(Fn && fn, Args&&... args);
	// 参数一fn通常是一个函数指针,表示该线程调用的函数
	// 参数二是可变模板参数,会自动解析,并传给我们参数函数指针所指向的函数
	thread t1(transaction, 100000);
	thread t2(transaction, 200000);

	// 线程等待(必须进行线程等待,除非子线程执行函数中进行了detach)
	t1.join();
	t2.join();

	cout << sum << endl;
	return 0;
}

        It is obviously the same piece of code, but the difference is so big; the result we originally expected was 300000; but there are various different results as above, usually we use locking to handle it; C++11 also provides our lock. Encapsulated; details are as follows;

5. Classification of locks

        C++11 specifically provides the following four locks. We mainly explain mutex and ordinary mutex locks. Because we know ordinary mutex locks, it is very simple to understand other locks;

        Mutex locks mainly have the following interfaces;

        lock means locking (blocking call), unlock means unlocking; try_lock means trying to apply for a lock, and returns false if it is not applied for. It is a non-blocking call; through these interfaces, we can make the above code thread-safe;

static int sum = 0;
mutex mx; // 创建锁变量

void transaction(int num)
{
	// 上锁
	mx.lock();
	for (int i = 0; i < num; i++)
	{
		sum++;
	}
	mx.unlock(); // 解锁
}

int main()
{
	thread t1(transaction, 100000);
	thread t2(transaction, 200000);

	t1.join();
	t2.join();

	cout << sum << endl;
	return 0;
}

6、lockguard

        Lockguard is a mutex that uses the RAII mechanism to encapsulate the lock. Through this mechanism, we can make the lock take effect in a certain local scope. The actual principle is implemented using the constructor and destructor of the class; because the constructor is defined in the object When called, the destructor is called when it goes out of scope. We can apply for a lock in the constructor function lock and unlock it in the destructor function unlock. The following is a lockguard I simulated and implemented;

#pragma once
#include <mutex>

template<class Lock>
class Lockguard
{
public:
	Lockguard(std::Lock& mt)
		:_mt(mt)
	{
		 _mt.lock();
	}
	~Lockguard()
	{
		_mt.unlock();
	}
private:
	std::Lock& _mt;
};

        In fact, there are such classes in the library; they are called lock_gurad and unique_lock respectively; lock_guard is almost the same as what we implemented above; and unique_lock just adds some more locking and unlocking interfaces for us;

        Next, we use lock_guard to upgrade the code we wrote before again; 

static int sum = 0;
mutex mx; // 创建锁变量

void transaction(int num)
{
	// 出作用域自动销毁
	lock_guard<mutex> lg(mx);
	for (int i = 0; i < num; i++)
	{
		sum++;
	}
}

int main()
{
	thread t1(transaction, 100000);
	thread t2(transaction, 200000);

	t1.join();
	t2.join();

	cout << sum << endl;
	return 0;
}

7. Condition variables

        The condition variables encapsulated in C++ are similar to the condition variables under Linux, but they are encapsulated to provide cross-platform functionality; the specific interface is shown in the figure below;

        We use a question to demonstrate the use of conditional variables;

Use two threads, one to print odd numbers and one to print even numbers, alternately printing to 100;

Guess you like

Origin blog.csdn.net/Nice_W/article/details/132152478