New features of lambda expression, thread library and atomic operation Library

Keywords: Lambda less Programming Windows

C++11

10. lambda expression []

For example, in C++98, if you want to sort elements in a data set, you can use std::sort method, as follows:

#include <algorithm>
#include <functional>
int main(){
	int array[] = {4,1,8,5,3,7,0,9,2,6};
	
	// By default, the output is ascending in order of less than comparison.
	std::sort(array, array+sizeof(array)/sizeof(array[0]));
	for (auto& e : array) {
		cout << e << " ";
	}
	cout << endl;
	
	// If you need to descend, you need to change the rules for comparing elements
	std::sort(array, array + sizeof(array)/sizeof(array[0]),greater<int>());
	for (auto& e : array) {
		cout << e << " ";
	}
	
	return 0;
}

Output results:

0 1 2 3 4 5 6 7 8 9
9 8 7 6 5 4 3 2 1 0

If the element to be sorted is a custom type, a user-defined sorting comparison rule is required:

struct Goods{
	string _name;
	double _price;
};

struct Compare{		//functor	
	bool operator()(const Goods& gl, const Goods& gr){
		return gl._price <= gr._price;
	}
};

int main(){
	Goods gds[] = { { "Apple", 2.1 }, { "Banana", 3 }, { "orange", 2.2 }, {"pineapple", 1.5} };
	sort(gds, gds+sizeof(gds) / sizeof(gds[0]), Compare());
	for (auto& e : gds) {
		cout << e._name << ':' << e._price << " ";
	}
	cout << endl;
	return 0;
}

Output results:

With the development of C++ grammar, people begin to think that the above writing method is too complex. Every time in order to implement an algorithmic algorithm, we have to write a class/imitation function again. If the logic of each comparison is different, we have to implement many classes, especially the naming of the same class, which brings great difference to programmers. Then. Therefore, Lambda expression appears in C++11 grammar:

int main(){
	Goods gds[] = { { "Apple", 2.1 }, { "intersect", 3 }, { "orange", 2.2 }, {"pineapple", 1.5} };
	
	sort(gds, gds + sizeof(gds) / sizeof(gds[0]), [](const Goods& l, const Goods& r){
		return l._price < r._price;
		}
	);
	return 0;
}

The above code is solved by lambda expression in C++11. It can be seen that lambda expression is actually an anonymous function.

grammar

Format:

[capture-list] (parameters) mutable -> return-type { statement }
  1. [capture-list]: Capture list.
    The list always appears at the beginning of the lambda function. The compiler determines whether the next code is a lambda function according to []. The capture list can capture variables in the context for the lambda function to use.
  2. (parameters): a list of parameters.
    Consistent with the parameter list of ordinary functions, if parameter passing is not required, it can be omitted together with ().
  3. Mutable: By default, the lambda function is always a const function, and mutable can cancel its constants. When using this modifier, the parameter list cannot be omitted (even if the parameter is empty).
  4. -> return-type: return value type.
    The return value type of a function is declared in the form of a trace return type, which can be omitted when there is no return value. When the return value type is clear, it can also be omitted, and the return type can be deduced by the compiler.
  5. {statement}: Function body.
    In the body of the function, all captured variables can be used in addition to its parameters.

Note: In lambda function definition, parameter list and return value type are optional parts, while capture list and function body can be empty.

  • So the simplest lambda function in C++11 is []{}; the lambda function can't do anything.
int main(){
	// The simplest lambda expression, which has no meaning
	[]{};
	
	// Omit parameter list and return value type, and return value type is deduced from compiler to int
	int a = 3, b = 4;
	[=]{return a + 3; };
	
	// Return value type is omitted and no return value type is found.
	auto fun1 = [&](int c){b = a + c; };		//Anonymous functions are represented here
	fun1(10);
	cout << a << " " << b << endl;
	
	// lambda function with perfect parts
	auto fun2 = [=, &b](int c)->int{ return b += a + c; };
	cout << fun2(10) << endl;
	
	// Copy capture x
	int x = 10;
	auto add_x = [x](int a) mutable { x *= 2; return a + x; };
	cout << add_x(10) << endl;
	return 0;
}

Output results:

3 13
26
30

From the above code, we can see that lambda expression can actually be understood as an anonymous function, which can not be called directly. If you want to call directly, you can assign it to a variable by auto.

Capture list

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

  • [var]: capturing variable var by means of value passing
  • [=]: Indicates that the value passing method captures all variables in the parent scope (including this)
  • [&var]: Represents the reference passing capture variable var
  • [&]: Represents that reference passing captures variables in all parent scopes (including this)
  • [this]: Represents the way the value is passed to capture the current this pointer

[Note]

  1. Parent scope is a block of statements containing lambda functions
  2. Grammatically, capture lists can be composed of multiple capture items and are separated by commas.
    For example: [=, & a, & b]: capturing variables A and B by reference passing, and capturing all other variables by value passing
    [&, a, this]: Value passing captures variables A and this, and reference captures other variables
  3. Capture lists do not allow variables to be passed repeatedly, otherwise compiler errors will occur.
    For example: [=, a]:= All variables have been captured by value passing, and a duplication has been captured.
  4. The lambda function capture list outside the block scope must be empty.
  5. The lambda function in block scope can only capture local variables in parent scope, and any non-local or non-local variables can cause compilation errors.
  6. lambda expressions cannot be assigned to each other, even if they appear to be of the same type.
void (*PF)();

int main(){
	auto f1 = []{cout << "hello world" << endl; };
	auto f2 = []{cout << "hello world" << endl; };
	
	f1 = f2; 	// Compilation failure - > prompt failed to find operator=()
	
	// Allow a lambda expression copy to be used to construct a new copy
	auto f3(f2);
	f3();
	
	// lambda expressions can be assigned to function pointers of the same type
	PF = f2;
	PF();
	return 0;
}

Comparison of function object and lambda expression

  • Function objects, also known as imitation functions, are class objects that overload operator() operators in classes.
class Rate{
public:
	Rate(double rate) 
	 : _rate(rate)
	{}
	double operator()(double money, int year){
		return money * _rate * year;
	}
private:
	double _rate;
};

int main(){
	// Functional object
	double rate = 0.49;
	Rate r1(rate);
	r1(10000, 2);
	
	// lambda
	auto r2 = [=](double monty, int year)->double {return monty * rate*year; };
	r2(10000, 2);
	return 0;
}

In terms of usage, function objects are exactly the same as lambda expressions.

The function object takes rate as its member variable and gives the initial value when defining the object. The lambda expression can capture the variable directly through the capture list.

  • In fact, the way that the underlying compiler handles lambda expressions is exactly the way that function objects are handled:
    If a lambda expression is defined, the compiler automatically generates a class in which operator() is overloaded.

11. Thread Library

One of the most important features of C++11 is that it supports threads, so that C++ does not need to rely on third-party libraries in parallel programming, and introduces the concept of atomic classes in atomic operations. To use threads in the standard library, you must include a < thread > header file that declares the std::thread thread thread class.

Official documents:[ http://www.cplusplus.com/reference/thread/thread/?kw=thread]

Demonstration:

#include <iostream>
#include <thread>

using namespace std;

void fun(){
	cout << "A new thread!" << endl;
}
int main(){
	thread t(fun);
	t.join();
	cout << "Main thread!" << endl;
	return 0;
}

Thread Startup

C++ Thread Library starts a thread by constructing a thread object, which contains the context of the thread runtime, such as thread function, thread stack, thread start state, thread ID, etc. All operations are encapsulated together and eventually passed to _beginthreadex() at the bottom level. Thread function implementation

[Note]: _beginthreadex is the underlying c function for creating threads in windows.

std::thread() creates a new thread that can accept any callable object type (with or without parameters), including lambda expressions (with or without variables), functions, function objects, and function pointers.

// Create threads using lambda expressions as thread functions
int main(){
	int n1 = 500;
	int n2 = 600;
	thread t([&](int addNum){
		n1 += addNum;
		n2 += addNum;
	}, 500);
	
	t.join();
	std::cout << n1 << ' ' << n2 << std::endl;
	return 0;
}

End of thread

After starting a thread, when the thread ends, how do you reclaim the resources used by the thread? The threadlibrary gives us two choices:

  1. Additive: join()
  • join(): actively awaits the termination of the thread. In the calling process join(), when the new thread terminates, join() cleans up the related resources, then returns, and the calling thread continues to execute downward. Because join() cleans up the thread's resources, the thread object has nothing to do with the destroyed thread, so the object of a thread can only use join() once at a time, and after you call join(), join() will return false.
  1. detach()
  • detach(): New threads are separated from calling threads and can no longer interact with new threads. It's like breaking up with a girlfriend, and then you won't have any contact (interaction) anymore, and the resources she consumes afterwards won't need you to pay for them. Calling joinable() at this point is bound to return false. Separated threads will run in the background, and their ownership and control will be given to the C++ runtime. At the same time, the C++ runtime guarantees that when the thread exits, its related resources can be recovered correctly.

[Note] The choice must be made before the thread object is destroyed, because the thread may have ended before you join or detach the thread, and if you detach it later, the thread may continue to run after the thread object is destroyed.

Atomic Operating Library

The most important problem of multithreading is the problem of sharing data (that is, thread security). If shared data is read-only, then no problem, because read-only operations will not affect the data, let alone involve data modification, so all threads will get the same data.
However, when one or more threads want to modify the shared data, there will be a lot of potential troubles, such as:

#include <iostream>
#include <thread>
using namespace std;

unsigned long sum = 0L;
void fun(size_t num){
	for (size_t i = 0; i < num; ++i)
		sum++;
}
int main(){
	cout << "Before joining,sum = " << sum << std::endl;
	thread t1(fun, 10000000);
	thread t2(fun, 10000000);
	t1.join();
	t2.join();
	cout << "After joining,sum = " << sum << std::endl;
	return 0;
}

Output sum: 11940842, error!!! Because threads are not safe!

The traditional solution in C++98 is that the shared modified data can be locked and protected.

#include <iostream>
#include <thread>
#include <mutex>
using namespace std;

std::mutex m;
unsigned long sum = 0L;
void fun(size_t num){
	for (size_t i = 0; i < num; ++i){
		m.lock();		//Lock up
		sum++;
		m.unlock();		//Unlock
	}
}
int main(){
	cout << "Before joining,sum = " << sum << std::endl;
	thread t1(fun, 10000000);
	thread t2(fun, 10000000);
	t1.join();
	t2.join();
	cout << "After joining,sum = " << sum << std::endl;
	return 0;
}

Output sum: 20000000

Although locking can be solved, one drawback of locking is that:

  • As long as one thread is in sum +, other threads will be blocked, which will affect the efficiency of the program, and if the lock is not well controlled, it will easily cause deadlock. So atomic operation is introduced in C++11!

When you need to use the above atomic operation variables, you must add a header file < atomic >

#include <iostream>
#include <thread>
#include <atomic>
using namespace std;

atomic_long sum{ 0 };
void fun(size_t num){
	for (size_t i = 0; i < num; ++i)
		sum++; // Atomic operation
}
int main(){
	cout << "Before joining, sum = " << sum << std::endl;
	thread t1(fun, 1000000);
	thread t2(fun, 1000000);
	t1.join();
	t2.join();

	cout << "After joining, sum = " << sum << std::endl;
	return 0;
}

Output sum: 20000000

Posted by mgmoses on Thu, 29 Aug 2019 04:28:30 -0700