可变参数模板+lambda+function包装器(适配器)+bind

目录

可变参数模板

引入

介绍 

展开参数包的方法 

递归

逗号表达式 

整体使用

emplace

介绍

​编辑

使用 

模拟实现

代码

示例

lambda

引入

介绍

格式

使用

传参

捕捉

原理

举例

function包装器(适配器) 

引入

介绍

格式

使用 

bind 

介绍

std::placeholders

使用 

修改参数位置

为参数传固定值


可变参数模板

引入

  • 还记得c语言中的printf吗,可以传入任意数量的变量来打印,非常的便捷
  • 而他正是用了c中的可变参数列表,不过和我们现在要介绍的可变参数模板的原理不一样
  • 我们只要知道可变参数模板很好用很灵活就是了,它让泛型编程更加完善
  • 但是,c++中的这个语法很抽象,学个基本也就差不多了

介绍 

template 
void ShowList(Args... args)
{}
  • 带...的参数称为"参数包",包含了0-n个模板参数
  • Args是传入的模板参数包,而args是函数形参参数包
  • 但我们无法像printf里那样做,printf是通过va_list对象和宏拿到它的参数的
  • 也无法使用args[i]来取参数
  • 这里,只能通过展开参数包来拿到参数

展开参数包的方法 

递归

// 递归终止函数
template 
void ShowList(const T& t) //当只有一个参数时
{
     cout << t << endl;
}

// 展开函数
template  
void ShowList(T value, Args... args) //多个参数时
{
     cout << value <<" ";
     ShowList(args...);
}

这里利用了函数重载,当调用showlist函数时:

  • 如果传入了多个参数,会匹配第二个函数,并且不断调用自己,拿到自己当前的第一个模板参数,这样,传入参数包的参数会不断减少一个,直到最后只剩一个模板参数时,调用第一个函数
  • 如果只传入一个参数,刚好就直接匹配到第一个函数(会找到最匹配的那个)
  • 如果要支持无参调用,可以将第一个函数改为无参(因为args可以是0个参数)

逗号表达式 

它不需要递归来获取,而是利用多个语法的特性,直接数组构造过程中把参数包展开

template 
void PrintArg(T t)
{
     cout << t << " ";
}

//展开函数
template 
void ShowList(Args... args)
{
     int arr[] = { (PrintArg(args), 0)... };
     cout << endl;
}
  • 这里是用初始化列表初始化一个数组,而里面的元素是逗号表达式+参数包
  • 所以,实际上它里面的元素展开后是 -- ((printarg(arg1),0), (printarg(arg2),0),(printarg(arg3),0)...)
  • 所以在构造的过程中,会一个一个执行逗号表达式
  • 首先,先执行逗号前的表达式,也就是调用函数,打印参数
  • 然后用逗号后的int值--也就是这里的0,作为该逗号表达式的返回值,初始化数组
  • 这样,一个数组构造完毕,参数包里的参数也都打印出来了

整体使用

但是,上面两种方法只能一个一个取出参数,没啥大用,最多就是打印出来

但是,如果我们能整体使用,就可以用来传参了!

class Date
{
public:
	Date(int year = 1, int month = 1, int day = 1)
		:_year(year)
		,_month(month)
		,_day(day)
	{
		cout << "Date构造" << endl;
	}

	Date(const Date& d)
		:_year(d._year)
		, _month(d._month)
		, _day(d._day)
	{
		cout << "Date拷贝构造" << endl;
	}

private:
	int _year;
	int _month;
	int _day;
};


template 
Date* Create(Args... args)
{
	Date* ret = new Date(args...);

	return ret;
}
  • create函数中,将传入的多个参数打包成参数包,直接用来构造date对象
  • 而参数包中的参数,可以用来匹配函数的参数,如果匹配,就可以直接调用该函数
  • 如果不匹配,就会报错
  • 而这个特性,就是emplace的基础
emplace
介绍

可变参数模板+lambda+function包装器(适配器)+bind_第1张图片

  • emplace系列的这两个函数,在多个stl容器中都有设置,而他,正使用了上面介绍的可变参数模板来插入任意个数个元素
使用 

这里我们用list的结点为pair来演示可变参数列表的作用

void test1()
{
    std::list> l;
    l.push_back(make_pair(1, "22"));
    l.push_back({1, "1434"});
    cout << endl;
    l.emplace_back(1, "111");
}
  • 在之前,我们可以使用make_pair/多参数的隐式类型转换 来传入pair对象
  • emplace系列可以直接传参

可变参数模板+lambda+function包装器(适配器)+bind_第2张图片

  • 因为push_back的参数只能是元素类型,也就是这里的pair类型
  • 所以,传入的非pair类型的参数,都得先构造一个临时的pair对象,才能进入该函数

  • 所以 -- 
  • 这里结果的上半截,都是先构造一个string,然后构造pair
  • 再在push_back内部,用pair调用结点的构造函数
  • 在构造函数中,又分别调用pair对象的first成员,second成员的拷贝构造,拷贝出一个pair对象给结点
  • 但又因为有移动拷贝的存在,所以,拷贝给结点的过程是用移动操作完成的

  • 结果的下半截则是用可变参数模板接收,和上面介绍的整体使用一样,参数包一直被传递到调用pair的拷贝构造那里
  • 然后,传入的参数个数和类型刚好能匹配pair的构造函数
  • 所以,就直接调用了string的构造,用传入的字符串初始化了pair的second成员
  • 这样就绕过拷贝操作,直接构造最底层调用

其实这里用自定义类型的话,效率差别不大,因为有移动操作的存在,它可以直接交换资源

但是,如果涉及到占用大内存,但全是内置类型的类,用emplace系列的函数,会大大提高效率(就类似上面的date类)

模拟实现

我们可以在自己实现的list中,也添加emplace函数

代码
        template 
        ListNode(Data &&val) //之前实现过的移动构造
            : _ppre(nullptr), _pnext(nullptr), _val(forward(val)){};

        template 
        ListNode(Args... args)
            : _ppre(nullptr), _pnext(nullptr), _val(args...) 
        //这样这里将可变参数模板直接传入构造val的构造函数里,如果匹配,就直接构造了
        {
        }


        template    
        void emplace_back (Args&&... args){ 
            insert(end(),args...); //将参数包传给insert
        }


        template   
        iterator insert(iterator pos, Args&&... args)
        {
            PNode cur = pos._pNode;
            PNode pre = cur->_ppre;
            PNode newnode = new Node(args...); //参数包传给node构造函数
 
            newnode->_pnext = cur;
            pre->_pnext = newnode;

            cur->_ppre = newnode;
            newnode->_ppre = pre;

            _size++;

            return newnode;
        }
示例

可变参数模板+lambda+function包装器(适配器)+bind_第3张图片

这里上半截我们比库里的多了两行

原因在于我们这里构造头结点的时候,也需要用构造string,只不过是默认构造;而库里是用的内存池

lambda

引入

我们之前已经学习了很多调用函数的方式,比如:函数指针,仿函数(实际上是类重载了())

  • 但是,这意味着,我们每使用一次函数,就要写一次函数体
  • 如果我们需要多次使用,功能类似但不同的函数该怎么办?定义很多份吗?
  • 这就会涉及到函数起名的问题
  • 功能类似真的好难取名,如果不好好取名,可读性就很差

而这里的lambda表达式,也可以像函数一样使用,它就能解决这个问题

介绍

是C++11引入的一种匿名函数或闭包表达式,它允许你在代码中内联定义函数,而无需显式命名函数

格式

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

[capture-list] 

  • 捕捉列表,该列表总是出现在lambda函数的开始位置,编译器根据[]来判断接下来的代码是否为lambda函数
  • 捕捉列表能够捕捉上下文中的变量供lambda函数使用
  • 默认情况下,捕捉到的是该变量的拷贝,且为const类型

(parameters)

  • 参数列表,与普通函数的参数列表的使用一致
  • 如果不需要参数传递,可以连同()一起省略

mutable

  • 因为前面已经介绍了,lambda函数默认下,捕捉到的变量是const类型,而mutable可以取消其常量(但注意,这里只是没有了常量性,但依然是变量的拷贝)
  • 使用该修饰符时,参数列表不可省略(即使参数为空)

->returntype

  • 返回值类型,追踪返回类型形式声明函数的返回值类型
  • 没有返回值/返回值明确时可省略(因为编译器可以对返回类型进行推,就像auto那样)

{statement}

  • 函数体
  • 在该函数体内,可以使用参数/捕获到的对象/全局域的函数,其他的无法使用

使用

传参

可以传值,也可以传引用

auto goodsPriceLess = [](const Goods& x, const Goods& y){return x._price < y._price; };

捕捉

默认下捕捉到的是const类型的拷贝

  • 可以使用mutable改变const类型,但依然是拷贝
  • 也可以直接传引用(&变量名),这样x既能修改,也能在外部得到这份修改:
  • int main() {
    	int x = 0;
    	auto a = [&x]() {++x; };
    	a();
    	cout << x << endl;
    	return 0;
    }
  • 如果想要捕捉到该作用域的全部变量时,可以使用[ = ] / [ & ]

  • [ = ] -- 用值捕捉到该作用域的所有变量

  • [ & ] -- 用引用捕捉到该作用域的所有变量

  • 可变参数模板+lambda+function包装器(适配器)+bind_第4张图片

  • 注意!在块作用域之外的lambda表达式,捕捉列表必须为空

原理

  • 实际上,我们的lambda表达式之间是无法赋值的,即使是完全一样的表达式
  • 而原因就是因为,它的底层是仿函数,每一个表达式都是不同的类对象,且类中重载了()运算符
  • 可变参数模板+lambda+function包装器(适配器)+bind_第5张图片
  • 所以,每一个lambda表达式都是不同的类型,所以无法赋值
  • 但是,它支持拷贝

  • lambda表达式都是匿名类对象,他们的名字和类型都是由编译器自动生成的
  • 名字是lambda+uuid(uuid是用了某种算法,生成的重复概率极小的字符串)
  • 而类型则是一个匿名的函数对象类型
  • 使用lambda表达式就是调用类中的operator()函数
  • 可变参数模板+lambda+function包装器(适配器)+bind_第6张图片

  • 如果一个函数指针的类型和lambda表达式类型是一样的,则lambda 表达式可以分配给函数指针(可能因为operator()函数具有与函数指针相似的签名(参数列表和返回类型))

举例

sort(v.begin(), v.end(), [](const Goods& x, const Goods& y) {return x._price > y._price;});

可以传给算法,比如sort中的排序,这样就不用自己专门写一个仿函数了

function包装器(适配器) 

引入

现在,我们已经知道了有三种调用函数的方式 -- 函数指针,仿函数,lambda表达式

那么使用类模板传参的时候,如果分别用了三种不同方式的函数,就会实例化出三份,但实际我们并不需要这么多份

而且如果我们想要将这些可调用类型存在容器中呢?实例化容器的时候类型该怎么写呢?函数指针和仿函数的类型都能写,但lambda的该怎么办呢?

所以,function包装器可以解决这个问题,它可以适配所有可调用的类型

介绍

头文件:

无需在编译时指定确切的类型,而是在运行时将不同类型的可调用对象分配给它

可变参数模板+lambda+function包装器(适配器)+bind_第7张图片

我们可以用function定义出的对象接收不同类型的可调用对象,只要返回值和参数列表相同即可:

可变参数模板+lambda+function包装器(适配器)+bind_第8张图片

格式

functioni<返回值(参数列表)>

使用 

int func(int x, int y)
{
	return x - y;
}
struct Function
{
	int operator()(int x, int y)
	{
		return x - y;
	}
};
void test2() {
	function sub1 = [](int x, int y) {return x - y; };
	function sub2 = func;
	function sub3 = Function();
	cout << sub1(2, 1) << endl;
	cout << sub2(3, 1) << endl;
	cout << sub3(4, 1) << endl;
}

和原先使用他们的方式一样,只不过接收它的是function类型

存放在容器中:

void test3() {
	vector< function> f;
	f.push_back(func);
	f.push_back(Function());
	f.push_back([](int x, int y) {return x - y; });
	cout << f[0](1, 2) << endl;
}

当我们存起来后,可以通过下标拿到可调用对象,再使用它执行函数功能

除此之外,可以用来指定命令执行某一函数

比如:当我们已经有了后缀表达式时,就可以直接进行计算,但是得分很多种情况,运算符越多,情况也越多

  • 如果直接使用判断的方式,效率太低
  • 我们可以利用function和map,直接拿到传入运算符的运算方式,然后传参即可
  • int evalRPN(vector& tokens) {
      stack st;
      map> opFuncMap =
     { 
     { "+", [](int i, int j){return i + j; } },
     { "-", [](int i, int j){return i - j; } },
     { "*", [](int i, int j){return i * j; } },
     { "/", [](int i, int j){return i / j; } }
     };
    
      for(auto& str : tokens)
     {
             if(opFuncMap.find(str) != opFuncMap.end())
             {
                 int right = st.top();
                 st.pop();
                 int left = st.top();
                 st.pop();
                 st.push(opFuncMap[str](left, right));
         }
             else
             {
                 // 1、atoi itoa
                 // 2、sprintf scanf
                 // 3、stoi to_string C++11
                 st.push(stoi(str));
             }
         }
         return st.top();
    }

bind 

介绍

用于创建函数对象(可以是前面提到的那三种)的绑定,允许你在调用函数时 预先绑定一些参数或改变参数顺序
它提供了一种便捷的方法来部分应用函数、改变参数顺序或创建函数对象 (也就是可以自由控制传参) 
可变参数模板+lambda+function包装器(适配器)+bind_第9张图片

std::placeholders

除此之外,bind的使用还需要配合std::placeholders这个作用域中定义的一组占位符对象,它用于指定绑定函数时的参数位置

可变参数模板+lambda+function包装器(适配器)+bind_第10张图片

使用 

修改参数位置

可变参数模板+lambda+function包装器(适配器)+bind_第11张图片

也就是说,这里会存在两层调用

  • 第一层 -- 新调用对象中的第一个参数传给bind中_1对象的位置
  • 第二层 -- 再将该参数传给对应位置的源对象函数形参(也就是这里的a)
  • 这里使用的时候并没有调换两参数的位置,但如果我们将bind中_1和_2的位置交换,即可完成参数位置的修改
为参数传固定值

实际上第一种情况我们用的并不多,更多的是自由地对形参传不同的固定值

double PPlus(int a, double rate, int b) { //这里我们将rate设置为固定传参
	return (a + b) * 1.0 * rate;
}

void test4() {
	function new_pplus = bind(PPlus, std::placeholders::_1, 2.3, std::placeholders::_2);
	new_pplus(1, 1);
}

当然,我们可以利用bind生成不同固定传参的变量,而不需要自己去修改函数代码

当我们想要给类的成员函数实现该功能时:

class SubType
{
public:
	static int sub(int a, int b)
	{
		return a - b;
	}

	int ssub(int a, int b, int rate)
	{
		return (a - b) * rate;
	}
};

由于bind函数接收一个函数对象+一系列参数,所以需要依靠类名拿到这两种函数的函数指针

  • 由于类的静态成员函数可以直接通过类域调用,所以函数指针就是subtype::sub
  • 而普通的成员函数必须要拿到地址才行,也就是&subtype::ssub
  • 但实际在使用时,都可以在前面+&,这样就不用可以去区分了
  • 最重要的一点!!!类的成员函数会将this指针隐式传入,而这个指针不是我们手动传入的,而是通过可调用对象,调用到该函数,然后将该对象的指针作为this指针

你可能感兴趣的:(c++,开发语言,c++,1024程序员节)