文章目錄
  1. 1. 列表初始化
    1. 1.1. 列表初始化概念
    2. 1.2. 列表初始化的使用细节
    3. 1.3. 初始化列表
    4. 1.4. 防止类型收窄
  2. 2. 基于范围的for循环
  3. 3. std::function和bind绑定器
    1. 3.1. 可调用对象
    2. 3.2. 可调用对象包装器—std::function
  4. 4. std::bind绑定器
  5. 5. lambda表达式
    1. 5.1. lambda表达式的概念和基本用法
  6. 6. tuple元组


作者:Frank
时间:2016-11-30

上一节讲述了C++中的auto关键字和decltype关键字以及函数模板参数等概念,在本节将继续对C++11中的新特性进行讲述,主要包括列表初始化、范围for语句、function和bind绑定器以及lambda表达式。

列表初始化

列表初始化概念

在C++98/03中,只有两种数据类型可以使用初始化列表,而在C++11中,初始化列表的适用性被大大增加了。它现在可以用于任意类型对象的初始化。其基本示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//ex1
class Foo{
public:
Foo(int){}
private:
Foo(const Foo&);
};
int main(void){
Foo a1(123);
Foo a2=123; //error 'Foo::Foo(const Foo&)' is private
Foo a3={123};
Foo a4{123};//a3 a4使用了新的初始化方式来初始化对象
int a5={3};
int a6{3};
return 0;
}

在C++11中,可以直接在变量名后面跟上初始化列表,来进行对象的初始化。而在初始化时,{}前面的等于号是否书写对初始化行为没有影响。在堆上动态分配的数组也可以使用初始化列表进行初始化。列表初始化还可以直接使用在函数的返回值上。

列表初始化的使用细节

能够使用列表初始化的类型需要满足其为聚合类型,而聚合类型的定义为:

  • 类型是一个普通数组;
  • 类型是一个类(class,struct,union),且(1)无用户自定义的构造函数;(2)无私有或保护的非静态数据成员;(3)无基类;(4)无虚函数;(5)不能有{}和=直接初始化的非静态数据成员。

初始化列表

在stl中,容器是通过std::initializer_list这个轻量级的类模板来完成上述功能的。在了解了std::initializer_list之后,再来看看它的一些特点,如下:

  1. 它是一个轻量级的容器类型,内部定义了iterator等容器必须的概念;
  2. 对于std::initializer_list而言,它可以接收任意长度的初始化列表,但要求元素必须是同种类型T(或可转换为T);
  3. 它有3个成员接口:size(),begin(),end();
  4. 它只能被整体初始化或赋值;

std::initializer_list的内部并不负责保存初始化列表中元素的拷贝,仅仅存储了列表中元素的引用而已。因此,不能这样使用初始化列表:

1
2
3
4
std::initializer_list<int> func(void){
int a=1,b=2;
return {a,b};
}

注:应当总是把std::initializer_list看做保存对象的引用,并在它持有对象的生存期结束之前完成传递。

防止类型收窄

类型收窄是指导致数据内容发生变化或者精度丢失的隐式类型转换。具体来说,类型收窄包括以下几种情况:

  1. 从一个浮点数隐式转换为一个整形数,如int i=2.2;
  2. 从高精度浮点数隐式转换为低精度浮点数,如从long double隐式转换为double或float;
  3. 从一个整形数隐式转换为一个浮点数,并且超过了浮点数的表示范围,如float x=(unsigned long long)-1;
  4. 从一个整形数隐式转换为一个长度较短的整形数,并且超出了长度较短的整形数的表示范围,如char x=65536;

在C++11中,可以通过列表初始化来检查及防止类型收窄。

基于范围的for循环

在C++03/98中,不同的容器和数组,遍历的方式不尽相同,写法也不同意,也不够简洁,而C++11基于范围的for循环以统一、简洁的方式来遍历容器和数组。其基本示例如下所示:

1
2
3
4
5
6
7
8
9
10
#include<iostream>
#include<vector>

int main(void){
std::vector<int> arr;
/// ....
for(auto it=arr.begin();it!=arr.end();it++)
std::cout<<*it<<std::endl;
return 0;
}

而在C++11中,基于范围的for循环的写法为:

1
2
3
4
5
6
7
8
9
10
#include<iostream>
#include<vector>

int main(void){
std::vector<int> arr;
/// ....
for(auto n:arr)//使用基于范围for的循环
std::cout<<n<<std::endl;
return 0;
}

std::function和bind绑定器

可调用对象

在C++中,可调用对象有如下几种定义:

  • 是一个函数指针;
  • 是一个具有operator()成员函数的类对象;
  • 是一个可被转换为函数指针的类对象;
  • 是一个类成员指针;

在C++11中,定义中的这些对象都被称为可调用对象,相应的,这些对象的类型被称为可调用对象类型。

可调用对象包装器—std::function

std::function是一个可调用对象的包装器,它是一个类模板,可以容纳除了类成员(函数)指针之外的所有可调用对象。通过制定它的模板参数,它可以用统一的方式处理函数、函数对象、函数指针,并允许保存和延迟执行。
其示例如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#include <iostream>
#include <functional>

void func(void){
std::cout<<__FUNCTION__<<std::endl;
}

class Foo{
public:
static int foo_func(int a){
std::cout<<__FUNCTION__<<"("<<a<<")";
return a;
}
};

class Bar{
public:
int operator()(int a){
std::cout<<__FUNCTION__<<"("<<a<<")";
return a;
}
};

int main(void){
std::function<void(void)> fr1=func; //绑定一个普通函数
fr1();

std::function<int(int)> fr2=Foo::foo_func;
std::cout<<fr2(123)<<std::endl;

Bar bar;
fr2=bar;//绑定一个仿函数
std::cout<<fr2(123)<<std::endl;//

return 0;
}

从上面的示例可以看出std::function的使用方法,当给std::function填入合适的函数签名(即一个函数类型,只包括返回值和参数表)之后,它就变成了一个可以容纳所有这一类调用方式的“函数包装器”。std::function可以取代函数指针的作用。因为它可以保存函数延迟执行,所以比较适合作为回调。同样,std::function还可以作为函数入参。其示例如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include<iostream>
#include<functional>

void call_when_even(int x, const std::function<void(int)>& f){
if(!(x&1))
f(x)'
}

void output(int x){
std::cout<<x<<" ";
}

int main(void){
for(int i=0;i<10;i++)
call_when_even(i,output);
std::cout<<std::endl;
return 0;
}

std::bind绑定器

std::bind用来将可调用对象与其参数一起进行绑定。绑定后的结果可以使用std::function进行保存,并延迟调用到任何我们需要的时候。通俗的来讲,它主要有两大作用:

  1. 将可调用对象与其参数一起绑定成一个仿函数;
  2. 将多元(参数个数为n,n>1)可调用对象转换成一元或者(n-1)元可调用对象,即只绑定部分参数;

其示例代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include<iostream>
#include<functional>

void call_when_even(int x, const std::function<void(int)>& f){
if(!(x&1))
f(x)'
}

void output(int x){
std::cout<<x<<" ";
}

void output_add_2(int x){
std::cout<<x+2<<std::endl;
}

int main(void){
{
auto fr=std::bind(output,std::placeholders::_1);
for(int i=0;i<10;i++)
call_when_even(i,fr);
std::cout<<std::endl;
}

{
auto fr=std::bind(output_add_2,std::placeholders::_1);
for(int i=0;i<10;i++)
call_when_even(i,fr);
std::cout<<std::endl;
}
return 0;
}

std::placeholder::_1是一个占位符,代表这个位置将在函数调用时,被传入的第一个参数所替代;因为有了占位符的概念,std::bind的使用非常灵活,其示例代码如下所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include<iostream>
#include<functional>

void output(int x,int y){
std::cout<<x<<" "<<y<<std::endl;
}

int main(void){
std::bind(output,1,2)();//输出1 2
std::bind(output,2,std::placeholders::_1)(1);//输出2 1
std::bind(output,std::placeholders::_1,2)(1);//输出1 2

std::bind(output,2,std::placeholders::_2)(1);//error 调用时没有第二个参数
std::bind(output,2,std::placeholders::_2)(1,2);//输出2 2 第一个参数被吞掉了

std::bind(output,std::placeholders::_1,std::placeholders::_2)(1,2);//输出1 2
std::bind(output,std::placeholders::_2,std::placeholders::_1)(1,2);//输出2 1
return 0;
}

上面对std::bind的返回结果直接调用。可以看到,std::bind可以直接绑定函数的所有参数,也可以绑定部分参数。在绑定部分参数的时候,通过使用std::placeholders,来决定空位参数将会属于调用发生时的第几个参数。
bind还有一个强大之处就是可以组合多个函数。示例如下:
假设要找到集合中大于5小于10的元素个数:
首先需要一个用来判断是否大于5的功能闭包,如下:

1
std::bind(std::greater<int>(),std::placeholders::_1,10);

这里std::bind返回的仿函数只有一个int参数。当输入了这个int参数之后,输入的int值将直接和5进行大小比较,并在大于5时返回true。
然后,需要一个判断是否小于10的功能闭包:

1
std::bind(std::less_equal<int>(),std::placeholders::_1,10);

有了这两个功能闭包后,只需要用逻辑与把他们连接起来:

1
2
using std::placeholders::_1;
std::bind(std::logical_and<bool>(),std::bind(std::greater<int>(),_1,5),std::bind(std::less_equal<int>(),_1,10);

然后就可以符合多个函数/闭包的功能:

1
2
3
using std::placeholders::_1;
auto f=std::bind(std::logical_and<bool>(),std::bind(std::greater<int>(),_1,5),std::bind(std::less_equal<int>(),_1,10);
int count=std::count_if(coll.begin(),coll.end(),f);

lambda表达式

lambda表达式是C++11最重要也是最常用的一个特性。其来源于函数编程的概念,也是现代编程语言的一个特点。lambda表达式有如下优点:

  1. 声明式编程风格:就地匿名定义目标函数或函数对象,不需要额外写一个命名函数或者函数对象。以更直接的方式去写程序,好的可读性和维护性;
  2. 简洁:不需要额外再写一个函数或者函数对象,避免了代码膨胀和功能分散,让开发者更加集中在手边的问题,同时也获得了更高的生产率;
  3. 在需要的时间和地点实现功能闭包,使程序更灵活;

lambda表达式的概念和基本用法

lambda表达式定义了一个匿名函数,并且可以捕获一定范围内的变量。lambda表达式的语法形式可以简单归纳如下:

1
[ capture ] ( params ) opt -> ret { body; };

其中,capture是捕获列表,params是参数列表,opt是函数选项,ret是返回值;body是函数体。
在C++11中,lambda表达式的返回值是通过前面介绍的返回值后置语法来定义的。因为lambda表达式的返回值很容易被推导,因此在C++11中允许省略其返回值定义。
lambda表达式可以通过捕获列表捕获一定范围内的变量:

  • []不捕获任何变量;
  • [&]捕获外部作用域中的所有变量,并作为引用在函数体中使用(按引用捕获);
  • [=]捕获外部作用域中的所有变量,并作为副本在函数体中使用(按值捕获);
  • [=,&foo]按值捕获外部作用域中的所有变量,并按引用捕获foo变量;
  • [bar]按值捕获bar变量,同时不捕获其他变量;
  • [this]捕获当前类中的this指针,让lambda表达式拥有和当前类成员函数同样的访问权限。捕获this的目的是可以在lambda中使用当前类中的成员函数和成员变量;
    lambda表达式在C++11中被称为“闭包类型”。它是一个特殊的,匿名的非nunion的类类型。
    因此,可以认为它是一个带有operator()的类,即仿函数。因此,可以使用std::function和std::bind来存储和操作lambda表达式:
    1
    2
    std::function<int(int)> f1=[](int a){return a;}
    std::function<int((void)> f2=std::bind([](int a){return a;});

没有捕获变量的lambda表达式可以直接转换为函数指针,而捕获变量的lambda表达式则不能转换为函数指针。

tuple元组

tuple元组是一个固定大小的不同类型值的集合,是泛化的std::pair。其基本使用示例如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
tuple<const char*,int> tp=make_tuple(sendPack,nSendSize);//构造一个tuple
//其相当于
struct tp{
char * p;
int len;
}

auto tp=return std::tie(1,"aa",2);//tp的实际类型是std::tuple<int&,string&,int&>

//获取tuple的值
const char* data=tp.get<0>();
int len=tp.get<1>();

//tie解包
int x,y;
string a;
std::tie(x,a,y)=tp;

//使用std::ignore占位符来表示不解其中某个位置的值
std::tie(std::ignore,std::ignore,y)=tp;

//创建右值的引用元组
std::map<int,std::string> m;
m.emplace(std::forward_as_tuple(10,std::string(20,'a')));

//tuple_cat连接多个tuple
std::tuple<int,std::string,float> t1(10,"Test",3.14);
int n=7;
auto t2=std::tuple_cat(t1,std::make_pair("Foo","bar"),t1,std::tie(n));
n=10;
print(t2);//输出(10,Test,3.14,Foo,bar,10,Test,3.14,10)

tuple虽然可以用来代替简单的结构体,但不要滥用,如果使用tuple来替代3个以上字段的结构体时就不太合适了,因为使用tuple会使得代码的易读性降低。



转载请注明出处

文章目錄
  1. 1. 列表初始化
    1. 1.1. 列表初始化概念
    2. 1.2. 列表初始化的使用细节
    3. 1.3. 初始化列表
    4. 1.4. 防止类型收窄
  2. 2. 基于范围的for循环
  3. 3. std::function和bind绑定器
    1. 3.1. 可调用对象
    2. 3.2. 可调用对象包装器—std::function
  4. 4. std::bind绑定器
  5. 5. lambda表达式
    1. 5.1. lambda表达式的概念和基本用法
  6. 6. tuple元组