【常见的C++11新特性(二)】可变模板参数、lambda表达式、包装器、bind

可变参数模板

C++11之前的函数模板和类模板中只能含有固定的参数数量,这使得我们在不确定参数个数的情况下,使用模板就会变得比较麻烦。而C++11提供了可变参数模板,允许模板能接受可变数量的模板参数,这大大提高了C++泛型编程的灵活性和通用性。下面介绍可变参数模板的原理和用法。

使用参数包

可变参数模板的核心思想是使用模板参数包和函数参数包。这些包可以包含0个或者是多个参数,通过展开来处理这些参数。其中模板参数包使用...来定义(函数模板也是一样),比如:

// Args是一个模板参数包,args是一个函数形参参数包
// 声明一个参数包Args...args,这个参数包中可以包含0到任意个模板参数。
template <class ...Args>
void ShowList(Args... args)
{}

当我们使用参数包时,参数的顺序是按照传入时的顺序保持不变的。在展开参数包时,参数会按照它们在包中的顺序依次展开
下面我们用简单的代码样例来证明:

递归函数方式展开参数包

来观察以下代码

#include<iostream>
using namespace std;
void print() {
	cout << "end" << endl;
}

template <class T,class... Args>
void print(T first, Args... args)
{
	cout << first << " ";
	print(args...);
}

int main() {
	print(1, 'a', 'b', 'c');
	return 0;
}

运行结果:
在这里插入图片描述

  • 为什么会出现这种结果

首次调用print函数时,print函数依次接收参数1,’a‘,'b','c',此时参数包args中包含的参数就是abc,实参1传递给了参数frist,于是第一个打印出来的参数就是1.接下来,把args整个参数包再传给print,按照顺序,依旧把第一个参数传递给first,剩下的参数打包又传递给了args。于是第二次print打印出来的first就是a,同样的,后面依次打印bc。那为什么还会打印end呢?这是因为递归到最后,args里没有参数了不会再调用模板实例化出来带参的print函数,此时print(args...);调用的是无参版的print函数,至此递归终止。这样我们就拿到了参数包中所有的参数。

  • 为什么要设计一个无参的print函数
    这是因为递归到最后参数包里面会没有参数,此时模板无法再实例化出一个符合的函数,就需要我们提前设计一个无参版的print函数来终止递归。

逗号表达式展开参数包

观察下面代码:

template <class T>
void PrintArg(T t)
{
	cout << t << " ";
}
//展开函数
template <class ...Args>
void ShowList(Args... args)
{
	int arr[] = { (PrintArg(args), 0)... };
	cout << endl;
}
int main()
{

	ShowList(1, 'A', std::string("sort"));
	return 0;
}

在这里插入图片描述
重点观察这一行代码:

int arr[] = { (PrintArg(args), 0)... };

(PrintArg(args), 0)是一个逗号表达式,这个表达式的结果是0.通过这种方式,每个args里的参数都会传递给PrintArg。更具体的,这行代码实际上会被转换成 ((printarg(arg1),0), (printarg(arg2),0), (printarg(arg3),0), ... ) , 之后arr数组的元素都是0,元素个数是args的参数个数。这样一来,PrintArg()函数接收到了args中的每一个参数并打印出来了。

对比insert和emplace系列接口

在STL容器中,insert系列其实就是我们常用的push_back这种,而emplace系列比如emplace_back,借助参数包完美转发将元素在容器内直接构造,中间没有其它构造和拷贝。这使得empace_back会更加高效,尤其是当需要构造复杂对象时。为什么emplace_back可以直接构造,这其实就是用到了参数包的原理。
以push_back和empalce_back为例,假设容器的元素类型为pair,分别观察其使用区别:

  • push_back:
vector<pair<int,int> v;
v.push_back({1,1});

这个过程会首先构造一个初始化列表对象,再转换并构造出一个pair匿名对象,最后将这个匿名对象传入push_back函数中完成插入元素。

  • emplace_back
vector<pair<int,int> v;
v.emplace_back(1,1);

注意这里参数的区别,由于使用了可变参数模板,emplace_bake不需要先构造出一个pair匿名对象,而是通过展开参数包将参数包里的参数直接传递给pair的构造函数构造一个pair对象,省略了初始化列表构造pair。

这就是可变参数模板的一个常见的应用。

lamdba表达式

Lambda表达式是C++11引入的一种特性,允许你在代码中定义匿名函数(即没有名字的函数)。这种表达式可以捕获其上下文中的变量,并在需要时进行传递和调用。

lamdba表达式的基本语法

lamdba表达式语法格式如下:

[capture](parameters)mutable -> return_type {
    // Function body
};

其中

  • capture:捕捉列表,用于指定哪些外部变量可以被lamdba表达式获取,换句话来说,捕捉列表中的变量可以在函数体中使用。参数列表在[]中,编译器根据[]来判断接下来的代码是否为lambda函数
  • parameters:参数列表。和普通函数的参数一样。如果不需要参数传递,则可以连同()一起省略。
  • mutable:默认情况下,lambda函数总是一个const函数,mutable可以取消其常量性。使用该修饰符时,参数列表不可省略(即使参数为空)。该关键字通常省略。
  • ->return_type:return_type是返回类型,可以连着->省略,编译器能够自动推导出来
  • function body:函数体,包含了lambda表达式要执行的代码。

来看下面的使用样例:

int main()
{
    // 最简单的lambda表达式, 该lambda表达式没有任何意义
   []{}; 
    
    // 省略参数列表和返回值类型,返回值类型由编译器推导为int
    int a = 3, b = 4;
   [=]{return a + 3; }; 
    
    // 省略了返回值类型,无返回值类型
    auto fun1 = [&](int c){b = a + c; }; 
    fun1(10);
    cout<<a<<" "<<b<<endl;
    
    // 各部分都很完善的lambda函数
    auto fun2 = [=, &b](int c)->int{return b += a+ c; }; 
    cout<<fun2(10)<<endl;
    
    // 复制捕捉x
    int x = 10;
    auto add_x = [x](int a) mutable { x *= 2; return a + x; }; 
    cout << add_x(10) << endl; 
    return 0;
}

其中最常用的就是下面这种

  auto fun2 = [=, &b](int c)->int{return b += a+ c; }; 
   cout<<fun2(10)<<endl;

使用auto fun2来让编译器自动推导出该函数对象的类型,在后面就像调用普通函数一样使用fun2。于是我们很容易想到,lamdba表达式能帮助我们在一个函数里面定义函数

关于捕捉列表

捕捉列表描述了上下文中那些数据可以被lambda使用,以及使用的方式传值还是传引用。

  • [var]:var表示外部的某个变量,表示传值方式捕捉变量var
  • [=]:表示传值方式捕捉父作用域中所有的变量包括this
  • [&var]:表示引用传递捕捉变量var,也就意味着在lamdba函数体中修改var也会影响到外部的var
  • [&]:表示引用传递捕捉所有父作用域中的变量(包括this)
  • [this]:表示值传递方式捕捉当前的this指针

值得注意的是

  • 父作用域指的是包含lambda函数的语句块 ,例如在man函数中定义一个lamdba,则该lamdba的父作用域就是main函数域。

  • 语法上捕捉列表可由多个捕捉项组成,并以逗号分割。例如[=, &a, &b]表示以引用传递的方式捕捉变量a和b,值传递方式捕捉其他所有变量。

  • 捕捉列表不允许变量重复传递,否则就会导致编译错误。比如:[=, a]:=已经以值传递方式捕捉了所有变量,捕捉a重复。这里要注意区分传值和传引用的区别。

  • lambda表达式之间不能相互赋值,即使看起来类型相同

函数对象与lamdba表达式

函数对象又被称为仿函数,借用重载()符号来使得用起来就像函数一样。从使用方式上看,仿函数和lamdba表达式完全一样。其实,lamdba的底层也是利用了仿函数的处理方式,即如果定义了一个lamdba表达式,编译器会自动生成一个类,在该类中重载了operator()

function包装器

function包装器 也叫作适配器。C++中的function本质是一个类模板,也是一个包装器,用于存储、传递和调用可调用对象(如函数、lambda表达式、函数对象等)

基本用法

语法格式如下:

function<返回类型(参数类型...)> fa;

fa就是一个包装器对象,存储了一个函数,函数的返回值类型和参数类型在 <> 中就已经声明了。

  • 包装普通函数
#include <iostream>
#include <functional>

void printMessage(const std::string& message) {
    std::cout << message << std::endl;
}

int main() {
    std::function<void(const std::string&)> func = printMessage;
    func("Hello, World!");
    return 0;
}

  • 包装lamdba表达式
    这种比较常用
#include <iostream>
#include <functional>

int main() {
    // 包装lambda表达式
    std::function<int(int, int)> add = [](int a, int b) {
        return a + b;
    };
    std::cout << "Sum: " << add(3, 4) << std::endl;
    return 0;
}

  • 包装仿函数
#include <iostream>
#include <functional>

// 函数对象
struct Multiply {
    int operator()(int a, int b) const {
        return a * b;
    }
};

int main() {
    // 包装函数对象
    std::function<int(int, int)> multiply = Multiply();
    std::cout << "Product: " << multiply(3, 4) << std::endl;
    return 0;
}

  • 包装成员函数
#include <iostream>
#include <functional>

class Printer {
public:
    void print(const std::string& message) {
        std::cout << message << std::endl;
    }
};

int main() {
    Printer printer;
    // 包装成员函数
    std::function<void(Printer&, const std::string&)> func = &Printer::print;
    func(printer, "Hello, Member Function!");
    return 0;
}

包装成员函数时需要注意,类中的非静态成员函数默认第一个参数类型是该类本身

bind函数

bind顾名思义就是绑定的意思,这个函数对象可以将部分参数绑定到某个函数上,生成一个新的可调用对象来“适应”原对象的参数列
比如我们用function对象存储了一个类的非静态成员函数,但是该函数每次调用都需要传递一个该类对象作为参数,我们其实可以在存储这个函数的时候绑定该类的对象参数,以后我们再拿function对象调用该函数时就不用再传递该类的对象作为参数了,编译器会帮你传。

用上面包装器包装成员函数的代码样例:

class Printer {
public:
    void print(const std::string& message) {
        std::cout << message << std::endl;
    }
};

int main() {
    Printer printer;
    // 包装成员函数
    std::function<void(const std::string&)> func = std::bind(&Printer::print, &printer,placeholders::_1);
    func("Hello, Member Function!");
    return 0;
}

  • 创建 Printer 类的对象 printer。

  • 使用 std::bind 将 Printer 类的 print 成员函数与 printer 对象绑定,并使用 std::placeholders::_1 占位符表示调用时需要传入的参数位置

  • 将绑定的成员函数存储在 std::function<void(const std::string&)> 类型的 func 变量中。

在这里插入图片描述

  • 7
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值