C++11
同一列表初始化
初始化列表时,可以不添加等号
struct Point
{
int _x;
int _y;
};
int main()
{
Point p = { 1, 2 };
Point p{ 1, 2 };//可以不使用 '='
int x2{ 2 };
int* p = new int[4]{0};
int* p1 = new int[4]{1, 2, 3, 4};
return 0;
}
initializer_list
#include <iostream>
#include <initializer_list>
int main()
{
auto li = { 10, 20, 30 };
initializer_list<int> li2 = { 1, 2, 3, 4 };
cout << typeid(li).name() << endl;
return 0;
}
int main()
{
auto li = { 10, 20, 30 };
initializer_list<int> li2 = { 1, 2, 3, 4 };
cout << typeid(li).name() << endl;
vector<int> v = { 1, 2, 3, 4, 5 };
list<int> l{ 10, 20, 30 };
//vector<Date> vd = { { 2001, 1, 1 }, { 2002, 2, 2 } };
map<string, int> dict = { make_pair("sort", 1), { "insert", 2 } };
return 0;
}
decltype
将变量的类型声明成指定的类型
int main()
{
int i = 10;
auto p = &i;
auto pf = strcpy;
cout << typeid(p).name() << endl;
cout << typeid(pf).name() << endl;
decltype(pf) px;
cout << typeid(px).name() << endl;
return 0;
}
//decltype的一些使用使用场景
template<class T1, class T2>
auto F(T1 t1, T2 t2) -> decltype(t1 * t2)//箭头指向返回值类型
{
decltype(t1 * t2) ret;
cout << typeid(ret).name() << endl;
return ret;
}
int main()
{
F(1, 2.2);
return 0;
}
右值引用
C++98中提出了引用的概念,引用即别名,引用变量与其引用实体公共同一块内存空间,而引用的底层是通过指针来实现的,因此使用引用,可以提高程序的可读性。
为了提高程序运行效率,C++11中引入了右值引用,右值引用也是别名,但其只能对右值引用。
左值与右值
可以放在=左边的,或者能够取地址的称为左值,只能放在=右边的,或者不能取地址的称为右值,但是也不一定完全正确。
右值eg:字面常量、表达式返回值,函数返回值等
- 1、普通类型的变量,因为有名字,可以取地址,都认为是左值。
- 2、 const修饰的常量,不可修改,只读类型的,理论应该按照右值对待,但因为其可以取地址(如果只是 const类型常量的定义,编译器不给其开辟空间,如果对该常量取地址时,编译器才为其开辟空间), C++11认为是左值。
- 3、 如果表达式的运行结果是一个临时变量或者对象,认为是右值。
- 4、 如果表达式运行结果或单个变量是一个引用则认为是左值。
总结:
- 不能简单地通过能否放在=左侧右侧或者取地址来判断左值或者右值,要根据表达式结果或变量的性质 判断
- 能得到引用的表达式一定能够作为引用,否则就用常引用。
int main()
{
double x = 1.1, y = 2.2;
// 以下几个都是常见的右值
10;
x + y;
fmin(x, y);
// 以下几个都是对右值的右值引用
int&& rr1 = 10;
double&& rr2 = x + y;
double&& rr3 = fmin(x, y);
cout << &rr1 << endl;
rr1 = 20;//没被存起来,因为是临时变量(右值不能取地址)
cout << &rr1 << endl;
// 这里编译会报错:error C2106: “=”: 左操作数必须为左值
//10 = 1;
//x + y = 1;
//fmin(x, y) = 1;
return 0;
}
引用与右值引用
普通引用只能引用左值,不能引用右值,const引用既可引用左值,也可引用右值。
C++11中右值引用:只能引用右值,一般情况不能直接引用左值。
右值不能取地址的,但是给右值取别名后,右值会被存储到特定的位置,且可以取到该位置的位置int main()
{
// 10纯右值,本来只是一个符号,没有具体的空间,
// 右值引用变量r1在定义过程中,编译器产生了一个临时变量,r1实际引用的是临时变量
int&& r1 = 10;
r1 = 100;
int a = 10;
int&& r2 = a; // 编译失败:右值引用不能引用左值
return 0;
}
// 1、左值引用 -》 左值
// 2、右值引用 -》 右值
// 3、const 左值引用 -》左值 或 右值
// 4、右值引用 -》 std::move(左值)
int main()
{
// 左值引用只能引用左值,不能引用右值。
int a = 10;
int& ra1 = a; // ra为a的别名
// int& ra2 = 10; // 编译失败,因为10是右值
const int& ra2 = 10;
const int& ra3 = 10 + 20;
return 0;
}
右值引用的场景和意义
namespace test
{
class string
{
public:
typedef char* iterator;
iterator begin()
{
return _str;
}
iterator end()
{
return _str + _size;
}
string(const char* str = "")
:_size(strlen(str))
, _capacity(_size)
{
//cout << "string(char* str)" << endl;
_str = new char[_capacity + 1];
strcpy(_str, str);
}
// s1.swap(s2)
void swap(string& s)
{
::swap(_str, s._str);
::swap(_size, s._size);
::swap(_capacity, s._capacity);
}
// 拷贝构造
string(const string& s)
:_str(nullptr)
{
cout << "string(const string& s) -- 深拷贝" << endl;
string tmp(s._str);
swap(tmp);
}
// 赋值重载
string& operator=(const string& s)
{
cout << "string& operator=(string s) -- 深拷贝" << endl;
string tmp(s);
swap(tmp);
return *this;
}
~string()
{
delete[] _str;
_str = nullptr;
}
char& operator[](size_t pos)
{
assert(pos < _size);
return _str[pos];
}
void reserve(size_t n)
{
if (n > _capacity)
{
char* tmp = new char[n + 1];
strcpy(tmp, _str);
delete[] _str;
_str = tmp;
_capacity = n;
}
}
void push_back(char ch)
{
if (_size >= _capacity)
{
size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;
reserve(newcapacity);
}
_str[_size] = ch;
++_size;
_str[_size] = '\0';
}
//string operator+=(char ch)
string& operator+=(char ch)
{
push_back(ch);
return *this;
}
const char* c_str() const
{
return _str;
}
private:
char* _str;
size_t _size;
size_t _capacity; // 不包含最后做标识的\0
};
test::string to_string(int value)
{
bool flag = true;
if (value < 0)
{
flag = false;
value = 0 - value;
}
test::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;//编译器把str识别成一个右值
}
}
//
// 左值引用的使用场景
// 1、做参数 -- 可以
// 2、做返回值 -- 有缺陷
void func1(test::string s)
{}
void func2(const test::string& s)
{}
int main()
{
test::string s1("hello world");
// func1和func2的调用我们可以看到左值引用做参数减少了拷贝,提高效率的使用场景和价值
//func1(s1);
func2(s1);
// operator+=可以使用传左值引用返回
s1 += 'A';
// to_string 不能用左值引用返回,这个就是左值引用短板
// 如果函数返回对象除了函数作用域就不在了,就不能使用做引用返回,就会存在拷贝
test::string ret1 = test::to_string(1234);
test::to_string(1234);
return 0;
}
左值引用的短板:当函数返回对象是一个局部变量,出了函数作用域就不存在了,就不能使用左值引用返回,只能传值返回。eg:test::to_string(int value) 这里只能传值返回,传值返回会导致至少1次拷贝构造(旧编译器是2次)
右值引用和移动语义解决以上问题: 移动构造本质是将参数右值的资源窃取过来占为己有,不需要深拷贝,所以叫移动构造,就是窃取别人资源来构造自己(没有开新空间拷贝数据,效率提高)
// 移动构造 增加一个参数是右值引用的版本
string(string&& s)
:_str(nullptr)
, _size(0)
, _capacity(0)
{
//转移资源
cout << "string(string&& s) -- 移动构造" << endl;
this->swap(s);
}
// 移动赋值
string& operator=(string&& s)
{
cout << "string& operator=(string&& s) -- 移动赋值" << endl;
this->swap(s);
return *this;
}
namespace test
{
class string
{
public:
typedef char* iterator;
iterator begin()
{
return _str;
}
iterator end()
{
return _str + _size;
}
string(const char* str = "")
:_size(strlen(str))
, _capacity(_size)
{
//cout << "string(char* str)" << endl;
_str = new char[_capacity + 1];
strcpy(_str, str);
}
// s1.swap(s2)
void swap(string& s)
{
::swap(_str, s._str);
::swap(_size, s._size);
::swap(_capacity, s._capacity);
}
// 拷贝构造
string(const string& s)
:_str(nullptr)
{
cout << "string(const string& s) -- 深拷贝" << endl;
string tmp(s._str);
swap(tmp);
}
// 移动构造 增加一个参数是右值引用的版本
string(string&& s)
:_str(nullptr)
, _size(0)
, _capacity(0)
{
//转移资源
cout << "string(string&& s) -- 移动构造" << endl;
this->swap(s);
}
// 移动赋值
string& operator=(string&& s)
{
cout << "string& operator=(string&& s) -- 移动赋值" << endl;
this->swap(s);
return *this;
}
// 赋值重载
string& operator=(const string& s)
{
cout << "string& operator=(string s) -- 深拷贝" << endl;
string tmp(s);
swap(tmp);
return *this;
}
~string()
{
delete[] _str;
_str = nullptr;
}
char& operator[](size_t pos)
{
assert(pos < _size);
return _str[pos];
}
void reserve(size_t n)
{
if (n > _capacity)
{
char* tmp = new char[n + 1];
strcpy(tmp, _str);
delete[] _str;
_str = tmp;
_capacity = n;
}
}
void push_back(char ch)
{
if (_size >= _capacity)
{
size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;
reserve(newcapacity);
}
_str[_size] = ch;
++_size;
_str[_size] = '\0';
}
//string operator+=(char ch)
string& operator+=(char ch)
{
push_back(ch);
return *this;
}
const char* c_str() const
{
return _str;
}
private:
char* _str;
size_t _size;
size_t _capacity; // 不包含最后做标识的\0
};
test::string to_string(int value)
{
bool flag = true;
if (value < 0)
{
flag = false;
value = 0 - value;
}
test::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;//编译器把str识别成一个右值
//使编译器优化,两次移动构造合二为一
}
}
//
// 左值引用的使用场景
// 1、做参数 -- 可以
// 2、做返回值 -- 有缺陷
void func1(test::string s)
{}
void func2(const test::string& s)
{}
int main()
{
test::string s1("hello world");
// func1和func2的调用我们可以看到左值引用做参数减少了拷贝,提高效率的使用场景和价值
//func1(s1);
func2(s1);
// operator+=可以使用传左值引用返回
s1 += 'A';
// to_string 不能用左值引用返回,这个就是左值引用短板
// 如果函数返回对象除了函数作用域就不在了,就不能使用做引用返回,就会存在拷贝
test::string ret1 = test::to_string(1234);
test::to_string(1234);
return 0;
}
总结:右值引用并不是直接使用右值引用去减少拷贝提高效率,而是支持深拷贝的类,提供移动构造和移动赋值,这时类的对象进行传值返回或参数为右值时,则可以用移动构造和移动赋值,转移资源,避免深拷贝
右值引用引用左值及更深入的分析
有些场景下,需要用右值去引用左值实现移动语义。当需要右值引用引用一个左值时,可以通过move函数将左值转化为右值。
int main()
{
test::string s1("hello world");
test::string s2 = s1;//左值 -- 深拷贝
test::string s3 = test::to_string(1234);//右值 -- 移动拷贝
test::string s4 = std::move(s1);//移动构造
return 0;
}
int main()
{
list<std::string> lt;
std::string s1("1111");
// 这里调用的是拷贝构造
lt.push_back(s1);
// 下面调用都是移动构造
lt.push_back("2222");
lt.push_back(std::string("2222"));
lt.push_back(std::move(s1));
return 0;
}
完美转发
完美转发是指在函数模板中,完全依照模板的参数的类型,将参数传递给函数模板中调用的另外一个函数。
完美转发是目标函数总希望将参数按照传递给转发函数的实际类型转给目标函数,而不产生额外的开销,就好像转发者不存在一样。
所谓完美:函数模板在向其他函数传递自身形参时,如果相应实参是左值,它就应该被转发为左值;如果相应实参是右值,它就应该被转发为右值。这样做是为了保留在其他函数针对转发而来的参数的左右值属性进
行不同处理(比如参数为左值时实施拷贝语义;参数为右值时实施移动语义)。
C++11通过forward函数来实现完美转发,
void Fun(int &x){ cout << "左值引用" << endl; }
void Fun(const int &x){ cout << "const 左值引用" << endl; }
void Fun(int &&x){ cout << "右值引用" << endl; }
void Fun(const int &&x){ cout << "const 右值引用" << endl; }
// 模板中的&&不代表右值引用,而是万能引用,其既能接收左值又能接收右值。
// 模板的万能引用只是提供了能够接收同时接收左值引用和右值引用的能力,
// 但是引用类型的唯一作用就是限制了接收的类型,后续使用中都退化成了左值,
// 我们希望能够在传递过程中保持它的左值或者右值的属性, 就需要完美转发
template<typename T>
void PerfectForward(T&& t)
{
Fun(std::forward<T>(t));
}
int main()
{
PerfectForward(10); // 右值
int a;
PerfectForward(a); // 左值
PerfectForward(std::move(a)); // 右值
const int b = 8;
PerfectForward(b); // const 左值
PerfectForward(std::move(b)); // const 右值
return 0;
}
template<class T>
struct ListNode
{
ListNode* _next = nullptr;
ListNode* _prev = nullptr;
T _data;
};
template<class T>
class List
{
typedef ListNode<T> Node;
public:
List()
{
//_head = new Node;
_head = (Node*)malloc(sizeof(Node));
_head->_next = _head;
_head->_prev = _head;
}
void PushBack(const T& x)
{
//Insert(_head, x);
Insert(_head, x);
}
void PushBack(T&& x)
{
//cout << &x << endl;
// 这里x属性退化为左值,其他对象再来引用x,x会识别为左值
//Insert(_head, x);
// 这里就要用完美转发,让x保持右值引用属性
Insert(_head, std::forward<T>(x));
}
void PushFront(T&& x)
{
//Insert(_head->_next, x);
Insert(_head->_next, std::forward<T>(x));
}
void Insert(Node* pos, T&& x)
{
Node* prev = pos->_prev;
//Node* newnode = new Node;
//newnode->_data = std::forward<T>(x); // 关键位置
Node* newnode = (Node*)malloc(sizeof(Node));
new(&newnode->_data)T(std::forward<T>(x));
// prev newnode pos
prev->_next = newnode;
newnode->_prev = prev;
newnode->_next = pos;
pos->_prev = newnode;
}
void Insert(Node* pos, const T& x)
{
Node* prev = pos->_prev;
//Node* newnode = new Node;
//newnode->_data = x; // 关键位置
Node* newnode = (Node*)malloc(sizeof(Node));
new(&newnode->_data)T(x);
// prev newnode pos
prev->_next = newnode;
newnode->_prev = prev;
newnode->_next = pos;
pos->_prev = newnode;
}
private:
Node* _head;
};
int main()
{
List<test::string> lt;
test::string s1("1111");
lt.PushBack("1111");
lt.PushFront("2222");
//右值引用的对象再作为实参传递时,属性会退化为左值,只能匹配左值引用
}
新的类功能
C++新增了两个:移动构造函数和移动赋值运算符重载
- 如果没实现移动构造函数,且没有实现析构函数、拷贝函数、拷贝赋值重载中的任意一个。那么编译器会自动生成一个默认移动构造。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看该成员是否实现移动构造,没有实现就用拷贝构造
- 如果没实现移动赋值重载函数,且没有实现析构函数、拷贝函数、拷贝赋值重载中的任意一个。那么编译器会自动生成一个默认移动赋值。默认生成的移动赋值函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看该成员是否实现移动赋值,没有实现就用拷贝构造
可变参数模板
//Args是一个参数模板包,args是一个参数形参参数包
//声明一个Args...args,这个参数包中可以包含0到任意个模板参数
template <class ...Args>
void Sh(Args...agrs)
{}
args前面有省略号,所以它是一个可变模板参数,我们把带省略号的参数称为参数包,它里面包含了0到N个模板参数。我们无法直接获取参数包args中的每一个参数,只能通过展开参数包的方式来获取参数包中的每一个参数。
template<class ... Args>
void Sh(Args ... args)
{
cout << sizeof ...(args) << endl;
}
int main()
{
Sh(1);
Sh(1, 'A');
Sh(1, 'B', std::string("insert"));
return 0;
}
递归防止展开参数包
/递归终止函数
void Sh()
{
cout << endl;
}
//展开函数
template<class T,class ... Args>
void Sh(T value,Args ... args)
{
//if(sizeof ...(args) == 0)
//{
// return;
//}
cout << value << " ";
Sh(args...);
}
template<class ... Args>
void Sh(Args ... args)
{
Sh(args...);
}
int main()
{
Sh(1);
Sh(1, 'A');
Sh(1, 'B', std::string("insert"));
return 0;
}
逗号表达式展开参数包
template <class T>
void PrintArgs(T t)
{
cout << t << " ";
}
//展开函数
template<class ... Args>
void sh(Args ... args)
{
//列表初始化
//{ (PrintArgs(arg1),0)... },{ (PrintArgs(arg2),0)... },{ (PrintArgs(arg3),0)... }。。。
int arr[] = { (PrintArgs(args),0)... };
cout << endl;
}
int main()
{
sh(1, 2, 3,4);//只能是整数
return 0;
}
emplace
int main()
{
// 下面我们试一下带有拷贝构造和移动构造的test::string
// 我们会发现其实差别也不到,emplace_back是直接构造了,push_back
// 是先构造,再移动构造,其实也还好。
std::list< std::pair<int, test::string> > mylist;
std::pair<int, test::string> kv(20, "sort");
mylist.emplace_back(kv); // 左值
mylist.emplace_back(std::pair<int, test::string>(20, "sort")); // 右值
mylist.emplace_back(10, "sort"); // 参数包
//cout << endl;
//mylist.push_back(kv); // 左值
//mylist.push_back(std::pair<int, test::string>(20, "sort")); // 右值
return 0;
}
lambda
struct Goods
{
string _name;
double _price;
int _num;
// ...
};
struct ComparePriceLess
{
bool operator()(const Goods& gl, const Goods& gr)
{
return gl._price < gr._price;
}
};
struct ComparePriceGreater
{
bool operator()(const Goods& gl, const Goods& gr)
{
return gl._price > gr._price;
}
};
struct CompareNumLess
{
bool operator()(const Goods& gl, const Goods& gr)
{
return gl._num < gr._num;
}
};
struct CompareNumGreater
{
bool operator()(const Goods& gl, const Goods& gr)
{
return gl._num > gr._num;
}
};
// 可调用对象类型
// 仿函数
// 函数指针
// lamber
int main()
{
vector<Goods> v = { { "苹果", 2.1, 300 }, { "香蕉", 3.3, 100}, { "橙子", 2.2 , 1000}, { "菠萝", 1.5, 1} };
// 要求分别按名字、价格、数量进行排序,升序或降序
sort(v.begin(), v.end(), ComparePriceLess());
sort(v.begin(), v.end(), ComparePriceGreater());
sort(v.begin(), v.end(), CompareNumLess());
sort(v.begin(), v.end(), CompareNumGreater());
return 0;
}
匿名的可调用对象
lambda表达式书写格式:
[capture-list] (parameters) mutable -> return-type { statement }
- 1、[capture-list] : 捕捉列表,该列表总是出现在lambda函数的开始位置,编译器根据[]来判断接下来的代码是否为lambda函数,捕捉列表能够捕捉上下文中的变量供lambda函数使用。
- 2、(parameters):参数列表。与普通函数的参数列表一致,如果不需要参数传递,则可以连同()一起省略
- 3、mutable:默认情况下,lambda函数总是一个const函数,mutable可以取消其常量性。使用该修
饰符时,参数列表不可省略(即使参数为空)。
4、->returntype:返回值类型。用追踪返回类型形式声明函数的返回值类型,没有返回值时此部分可省略。返回值类型明确情况下,也可省略,由编译器对返回类型进行推导。
5、{statement}:函数体。在该函数体内,除了可以使用其参数外,还可以使用所有捕获到的变量。
注意: 在lambda函数定义中,参数列表和返回值类型都是可选部分,而捕捉列表和函数体可以为空。
因此最简单的lambda函数为:[]{}; 该lambda函数不能做任何事情。
int main()
{
[]{};
int a = 1, b = 2;
// 实现add的lambda
auto add1 = [](int x, int y)->int{return x + y; };
cout << add1(a, b) << endl;
// 在捕捉列表,捕捉a、b, 没有参数可以省略参数列表,返回值可以通过推演,也可以省略
//auto add2 = [a, b]{}->int{return a + b + 10; };
auto add2 = [a, b]{return a + b + 10; };
cout << add2() << endl;
return 0;
}
int main()
{
vector<Goods> v = { { "苹果", 2.1, 300 }, { "香蕉", 3.3, 100 }, { "橙子", 2.2, 1000 }, { "菠萝", 1.5, 1 } };
// 要求分别按名字、价格、数量进行排序,升序或降序
// 如果仿函数存在命名不规范问题
/*sort(v.begin(), v.end(), ComparePriceLess());
sort(v.begin(), v.end(), ComparePriceGreater());
sort(v.begin(), v.end(), CompareNumLess());
sort(v.begin(), v.end(), CompareNumGreater());*/
/*auto f1 = [](const Goods& g1, const Goods& g2)
{
return g1._price > g2._price;
};
sort(v.begin(), v.end(), f1);
*/
sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2){return g1._price < g2._price; });
sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2){return g1._price > g2._price; });
sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2){return g1._num > g2._num; });
sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2){return g1._num < g2._num; });
return 0;
}
捕获列表说明:
捕捉列表描述了上下文中那些数据可以被lambda使用,以及使用的方式传值还是传引用。
- [var]:表示值传递方式捕捉变量var
- [=]:表示值传递方式捕获所有父作用域中的变量(成员函数中包括this)
- [&var]:表示引用传递捕捉变量var [&]:表示引用传递捕捉所有父作用域中的变量(成员函数中包括this)
- [this]:表示值传递方式捕捉当前的this指针
注意:
-
a. 父作用域指包含lambda函数的语句块
-
b. 语法上捕捉列表可由多个捕捉项组成,并以逗号分割。
eg:[=, &a,&b]:以引用传递的方式捕捉变量a和b,值传递方式捕捉其他所有变量 eg:[&,a, this]:值传递方式捕捉变量a和this,引用方式捕捉其他变量 -
c. 捕捉列表不允许变量重复传递,否则就会导致编 译错误。
eg:[=, a]:=已经以值传递方式捕捉了所有变量,捕捉a重复 -
d. 在块作用域以外的lambda函数捕捉列表必须为空。
-
e. 在块作用域中的lambda函数仅能捕捉父作用域中局部变量,捕捉任何非此作用域或者非局部变量都 会导致编译报错。
-
f. lambda表达式之间不能相互赋值,即使看起来类型相同
lambda表达式底层被编译器转化成了仿函数
包装器
function包装器也叫做适配器。C++中的function本质是一个类模板,也是一个包装器
可调用的对象类型:函数指针,仿函数(函数对象),lambda
// 函数模板会被实例化多次
template<class F, class T>
T useF(F f, T x)
{
static int count = 0;
cout << "count:" << ++count << endl;
cout << "count:" << &count << endl;
cout << "-------------------" << endl;
return f(x);
}
double func(double i)
{
return i / 2;
}
struct Functor
{
double operator()(double d)
{
return d / 3;
}
};
int main()
{
// 函数名
cout << useF(func, 11.11) << endl;
// 函数对象
cout << useF(Functor(), 2) << endl;
// lamber表达式
cout << useF([](double d)->double{ return d / 3; }, 11.11) << endl;
//useF函数被实例了3次
//使用包装器可以解决
return 0;
}
#include <functional>
//类模板原型
template <class T> function;//undifine
template <class Ret,class ... Args>
class function<Ret(Args)>
//Ret:被调用函数的返回类型
//Args: 被调用函数的形参
int f(int a, int b)
{
return a + b;
}
struct Functor1
{
public:
int operator() (int a, int b)
{
return a + b;
}
};
class Plus
{
public:
static int plusi(int a, int b)
{
return a + b;
}
double plusd(double a, double b)
{
return a + b;
}
};
int main()
{
//包装函数指针
std::function<int(int, int)> ff = f;
cout << ff(1, 2) << endl;
//包装仿函数
std::function<int(int, int)> ff2 = Functor1();
cout << ff2(11, 22) << endl;
//包装成员函数
std::function<int(int, int)> ff3 = Plus::plusi;//静态成员函数
cout << ff3(111, 222) << endl;
std::function<double(Plus,double, double)> ff4 = &Plus::plusd;
cout << ff4(Plus(), 6.6, 7.7) << endl;
//包装lambda
auto f5 = [](int a, int b){return a + b; };
std::function<int(int, int)> ff5 = f5;
cout << ff5(2.2, 3.3) << endl;
return 0;
}
/ 函数模板会被实例化多次
template<class F, class T>
T useF(F f, T x)
{
static int count = 0;
cout << "count:" << ++count << endl;
cout << "count:" << &count << endl;
cout << "-------------------" << endl;
return f(x);
}
double func(double i)
{
return i / 2;
}
struct Functor
{
double operator()(double d)
{
return d / 3;
}
};
int main()
{
// 函数名
std::function<double(double)> f1 = func;
cout << useF(f1, 11.11) << endl;
// 函数对象
std::function<double(double)> f2 = Functor();
cout << useF(f2, 2) << endl;
// lamber表达式
std::function<double(double)> f3 = [](double d)->double{ return d / 3; };
cout << useF(f3, 11.11) << endl;
//useF函数被实例了3次
//使用包装器可以解决
return 0;
}
总结: std::function 包装各种可调用的对象,统一可调用对象类型,并且指定了参数和返回值类型
为什么要有std::function? 答:因为不包装前可调用类型存在很多问题: 1、函数指针类型太复杂,不方便理解和使用 2、仿函数类型是一个类名,没有指定调用参数和返回值。需要去看operator()的实现才能知道 3、lambda表达式在语法层看不到类型
bind
bind是一个函数模板,它像一个函数包装器(适配器),接受一个可调用对象,生成一个新的可调用对象来“适应”原对象的参数列表。
可以把它一个原本接受N个参数的函数Fn,通过绑定一些参数,返回一个接受M个参数的新函数。同时,bind函数还可以实现参数顺序调序等操作
template<class Fn,class ...Args>
bind(Fn&&,Args&& ...args);
//
template<class Ret,class Fn,class ... Args>
bind(Fn&& fn,Args&& ...args);
调用bind:
auto newCallable = bind(callable,arg,list);
- newCallable:可调用对象
- arg,list:用逗号分割的参数列表
- callable:调用newCallable时,newCallable会调用callable,并传给它arg,list中的参数
int Plus(int a, int b)
{
return a + b;
}
class Sub
{
public:
int sub(int a, int b)
{
return a - b;
}
};
int main()
{
bind(Plus, placeholders::_1, placeholders::_2);//placeholders:命名空间
std::function<int(int)> f2 = bind(Plus, 10, placeholders::_1);//+10
cout << f2(5) << endl;
//绑定固定的可调用对象
std::function<int(int, int)> f3 = bind(&Sub::sub, Sub(), placeholders::_1, placeholders::_2);
cout << f3(1, 2) << endl;
//
std::function<int(int, int)> f4 = bind(&Sub::sub, Sub(), placeholders::_2, placeholders::_1);
cout << f4(1, 2) << endl;
return 0;
}
线程
mutex mtx;
void f(int N)
{
mtx.lock();
for (int i = 0; i < N; ++i)
{
cout <<this_thread::get_id()<<":" <<i << endl;
}
mtx.unlock();
}
int main()
{
int n;
cin >> n;
vector<thread> vthreads;
vthreads.resize(n);
for (auto& td : vthreads)
{
td = thread(f, 100);
//cout << td.get_id() << endl;
}
for (auto& td : vthreads)
{
td.join();
}
return 0;
}
原子性操作库(atomic)
多线程最主要的问题是共享数据带来的问题(即线程安全)。如果共享数据都是只读的,那么没问题,因为只读操作不会影响到数据,更不会涉及对数据的修改,所以所有线程都会获得同样的数据。但是,当一个或多个线程要修改共享数据时,就会产生很多潜在的麻烦。
C++98中传统的解决方式:可以对共享修改的数据可以加锁保护。虽然加锁可以解决,但是加锁有一个缺陷就是:只要一个线程在对x++时,其他线程就会被阻塞,会影响程序运行的效率,而且锁如果控制不好,还容易造成死锁。
int main()
{
int n;
cin >> n;
mutex mtx;
vector<thread> vthreads;
vthreads.resize(n);;
int N = 100;
int x = 0;//把x 变成原子的
for (auto& td : vthreads)
{
td = thread([&mtx, &N,&x]
{
mtx.lock();
for (int i = 0; i < N; i++)
{
++x;
}
mtx.unlock();
}
);
}
for (auto& td : vthreads)
{
td.join();
}
printf("%d个线程并行对x++了%d次,x == %d\n", n, N, x);
return 0;
}
因此C++11中引入了原子操作。所谓原子操作:即不可被中断的一个或一系列操作,C++11引入的原子操作类型,使得线程间数据的同步变得非常高效。
int main()
{
int n;
cin >> n;
mutex mtx;
vector<thread> vthreads;
vthreads.resize(n);;
int N = 100;
atomic<int> x = 0;//把x 变成原子的
for (auto& td : vthreads)
{
td = thread([&mtx, &N,&x]
{
for (int i = 0; i < N; i++)
{
++x;
}
}
);
}
for (auto& td : vthreads)
{
td.join();
}
printf("%d个线程并行对x++了%d次,x == %d\n", n, N, x);
return 0;
}
不需要对原子类型变量进行加锁解锁操作,线程能够对原子类型变量互斥的访问。更为普遍的,程序员可以使用atomic类模板,定义出需要的任意原子类型。
注意:原子类型通常属于"资源型"数据,多个线程只能访问单个原子类型的拷贝,因此在C++11中,原子类型只能从其模板参数中进行构造,不允许原子类型进行拷贝构造、移动构造以及operator=等,为了防止意外,标准库已经将atmoic模板类中的拷贝构造、移动构造、赋值运算符重载默认删除掉了。
//无论是正常执行结果,还是中途返回,还是抛异常
//智能指针lg都会在出了func函数作用域,都会调用自己的析构函数,
//保证一定能解锁
// RAII
namespace RAII
{
template<class Lock>
class lock_guard
{
public:
lock_guard(Lock& lock)
:_lock(lock)
{
_lock.lock();
cout << "加锁" << endl;
}
/*void lock()
{
_lock.lock();
}
void unlock()
{
_lock.unlock();
}*/
~lock_guard()
{
_lock.unlock();
cout << "解锁" << endl;
}
lock_guard(const lock_guard<Lock>& lock) = delete;
private:
Lock& _lock;
};
}
mutex mtx;
void func()
{
// 无论是正常执行?还是中途返回?还是抛异常?
// 如何保证这里锁一定解锁了?
//mtx.lock();
RAII::lock_guard<mutex> lg(mtx);
// ...
FILE* fout = fopen("test.txt", "r");
if (fout == nullptr)
{
// ....
//mtx.unlock();
return;
}
int n;
cin >> n;
char* p = new char[n]; // 抛异常
// f1(); // 抛异常
// ...
//mtx.unlock();
}
mutex mtx;
void func()
{
// 无论是正常执行还是中途返回还是抛异常
// 如何保证一定解锁
//mtx.lock();
{
RAII::lock_guard<mutex> lg(mtx);
FILE* fout = fopen("test.txt", "r");
if (fout == nullptr)
{
// ....
//mtx.unlock();
//return;
}
}
int n;
cin >> n;
char* p = new char[n]; // 抛异常
// f1(); // 抛异常
// ...
//mtx.unlock();
}
int main()
{
try{
func();
}
catch (exception& e)
{
cout << e.what() << endl;
}
return 0;
}
lock_guard
lock_guard类模板主要是通过RAII的方式,对其管理的互斥量进行了封装,在需要加锁的地方,只需要用任意互斥体实例化一个lock_guard,调用构造函数成功上锁,出作用域前,lock_guard对象要被销毁,调用析构函数自动解锁,可以有效避免死锁问题。
lock_guard的缺陷:太单一,用户没有办法对该锁进行控制,因此C++11又提供了unique_lock。
unqiue_lock
unique_lock类模板也是采用RAII的方式对锁进行了封装,并且也是以独占所有权的方式管理mutex对象的上锁和解锁操作,即其对象之间不能发生拷贝。在构造(或移动(move)赋值)时,unique_lock 对象需要传递一个 Mutex 对象作为它的参数,新创建的 unique_lock 对象负责传入的 Mutex对象的上锁和解锁操作。使用以上类型互斥量实例化unique_lock的对象时,自动调用构造函数上锁,unique_lock对象销毁时自动调用析构函数解锁,可以很方便的防止死锁问题。
两个线程交替打印,一个打印奇数,一个打印偶数
#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>//条件变量
using namespace std;
//创建两个线程交替打印,一个打印奇数,一个打印偶数
//int main()
//{
// int n = 100;
// mutex mtx;
// thread t1([&](){
// int i = 1;
// for (; i < n; i += 2)
// {
// unique_lock<mutex> lock(mtx);
// cout << i << endl;
// }
// });
// //假设线程2没创建好,或者没排到cpu时间片,就会导致t1连续打印奇数不符合要求
// thread t2([&](){
// int j = 2;
// for (; j < n; j += 2)
// {
// unique_lock<mutex> lock(mtx);
// cout << j << endl;
// }
// });
// t1.join();
// t2.join();
// return 0;
//}
int main()
{
int n = 100;
mutex mtx;
condition_variable cv;
bool flag = true;//通过flag打印交替
thread t1([&](){
int i = 1;
for (; i < n; )
{
unique_lock<mutex> lock(mtx);
cv.wait(lock, [flag]()->bool{return flag; });
cout << i << endl;
i += 2;
flag = false;
cv.notify_one();
}
});
thread t2([&](){
int j = 2;
for (; j < n; )
{
unique_lock<mutex> lock(mtx);
cv.wait(lock, [flag]()->bool{return !flag; });
cout << j << endl;
j += 2;
flag = true;
cv.notify_one();
}
});
t1.join();
t2.join();
return 0;
}