[C++11#46](三) 详解lambda | 可变参数模板 | emplace_back | 默认的移动构造

2024-09-04 16:52

本文主要是介绍[C++11#46](三) 详解lambda | 可变参数模板 | emplace_back | 默认的移动构造,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

目录

一.lambda

1. 捕捉列表

2. 底层原理

二. 可变参数模板

1. 递归函数方式展开参数包

2. 数组接收方式展开参数包

3. 运用

4.emplace_back

5.移动构造和拷贝构造

强制生成 default


一.lambda

可调用类的对象

  • 函数指针--少用 void(*ptr) (int x)
  • 仿函数--构造类 重载 operator() 对象可以像函数一样使用--eg.模板参数
struct ComparePriceGreater
{bool operator()(const Goods& gl, const Goods& gr){return gl._price > gr._price;}
};
int main()
{vector<Goods> v = { { "苹果", 2.1, 5 }, { "香蕉", 3, 4 }, { "橙子", 2.2, 3 }, { "菠萝", 1.5, 4 } };sort(v.begin(), v.end(), ComparePriceGreater());
}
  • lambda--匿名函数对象 函数内部,直接定义使用

相当于一个局部的没有函数名的函数对象

int main()
{auto compare = [](int x, int y) {return x > y; };cout << compare(1, 2) << endl;//可以像函数一样调用return 0;
}

内部可以调用全局函数吗?可以

局部函数呢?不可以

1. 捕捉列表

捕捉的方式

  1. [a,b] 传值捕捉
  2. [&a,&b] 传引用捕捉
  3. [=] 传值捕捉方式父作用域中所有变量(包括this指针)
  4. [&] 传引用捕捉方式父作用域中所有变量(包括this指针)
  5. 混合使用,例如捕捉[&x,y]

测试:

int main()
{int a = 0, b = 1;auto add1 = [](int x, int y) { return x + y; };cout << add1(a, b) << endl;auto add2 = [b](int x) {return x + b; };cout << add2(a) << endl;//引用的方式捕捉auto swap = [&a, &b](){int tmp = a;a = b;b = tmp;};swap();cout << "After swapping: a = " << a << ", b = " << b << endl;return 0;
}

特例:

  1. [=, &a, &b]:使用值传递方式捕获所有变量,除了 ab,这两个变量将以引用方式捕获。
  2. [&,a, this]:使用引用传递方式捕获变量 athis,其他变量将以值传递方式捕获。
  3. [=, a]:这个捕捉列表是错误的,因为 = 已经以值传递方式捕获了所有变量,而 a 也被重复捕获了。
  4. 在块作用域以外的lambda函数:捕捉列表必须为空。这意味着lambda不能捕获任何外部变量。
  5. 在块作用域中的lambda函数:只能捕获父作用域中局部变量。
  6. lambda表达式之间不能相互赋值:即使看起来类型相同,也不能直接将一个lambda表达式赋值给另一个。这是因为lambda表达式有特定的捕获行为和生命周期,它们不能像普通变量那样直接赋值。
int main(){int a = 0;int b = 1;int c = 2;int d = 3;const int e = 1;cout << &e << endl;// 引用的方式捕捉所有对象,除了a// a用传值的方式捕捉auto func = [&, a] {//a++;b++;c++;d++;//e++;cout << &e << endl;//const & 得到的是同一个地址};func();return 0;
}

所以对于局部函数我们也可以捕捉后调用,但是没什么必要

2. 底层原理

UUID:唯一识别码

打印查看仿函数:

#include <iostream>class Add {
public:int operator()(int a, int b) const {return a + b;}
};int main() {Add add;int result = add(10, 20);std::cout << "Result: " << result << std::endl;return 0;
}

底层:

将 lambda 和仿函数的底层对比查看:

int main()
{auto f1 = [](int x, int y) {return x + y; };auto f2 = [](int x, int y) {return x + y; };//f1 = f2;cout << typeid(f1).name() << endl;cout << typeid(f2).name() << endl;f1(1, 2);return 0;
}

底层:

实际在底层编译器对于lambda表达式的处理方式,完全就是按照函数对象的方式处理的

  • lambda_uuid(如上编译器没显示)
  • 即:如果定义了一个lambda表达式,编译器会自动生成一个类,在该类中重载了operator()

编译器底层只有类和仿函数 operate()


二. 可变参数模板

例如 printf,想要几个参数就传几个

  • 模板参数:类型
  • 函数参数:对象

表示为  ...

//模板可变参数
template <class ...Args>
void CppPrint(Args... args)
{cout << sizeof...(args) << endl;//打印参数个数
}
int main()
{CppPrint();CppPrint(1);CppPrint(1, 2);CppPrint(1, 2, 2.2);CppPrint(1, 2, 2.2, string("xxxx"));// ...return 0;
}

我有一技: 可以对可变参数进行打印吗?不可以

类似实现 CppPrint:

要通过编译时的函数重载,递归推演来打印

1. 递归函数方式展开参数包

  • 给函数模板增加一个模板参数这样就可以从接收到的参数包中分离出一个参数出来
    • 在函数模板中递归调用该函数模板,调用时传入剩下的参数包。
    • 如此递归下去,每次分离出参数包中的一个参数,直到参数包中的所有参数都被取出来
  • 还需要给一个递归终止函数
void _ShowList()
{// 结束条件的函数cout << endl;
}template <class T, class ...Args>
void _ShowList(T val, Args... args)
{cout << val << " ";_ShowList(args...);
}//args代表0-N的参数包
template <class ...Args>
void CppPrint(Args... args)
{_ShowList(args...);
}
//传给_,递归挨个解析出第一个值int main()
{CppPrint(1);CppPrint(1, 2);CppPrint(1, 2, 2.2);CppPrint(1, 2, 2.2, string("xxxx"));// ...return 0;
}

2. 数组接收方式展开参数包

还有一种新方式:利用编译器去推演,数组存储,可变参数包一个一个的取出来

  • 这种展开参数包的方式,不需要通过递归终止函数,是直接在expand函数体中展开的, printarg不是一个递归终止函数,只是一个处理参数包中每一个参数的函数
template <class T>
void PrintArg(T t)
{cout << t << " ";
}// 参数包有几个值,就展开调用几次
template <class... Args>
void ShowList(Args... args)
{int arr[] = {PrintArg(args)...};//利用编译器去推演,数组存储cout << endl;
}int main()
{ShowList(1);ShowList(1, 'A');ShowList(1, 'A', std::string("sort"));return 0;
}

3. 运用

模板的可变参数实现调用的多样化,灵活

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 <class ...Args>
Date* Create(Args... args)
{Date* ret = new Date(args...);return ret;
}int main()
{Date* p1 = Create();Date* p2 = Create(2023);Date* p3 = Create(2023, 9);Date* p4 = Create(2023, 9, 27);
//拷贝构造Date d(2023, 1, 1);Date* p5 = Create(d);return 0;
}

4.emplace_back

带模板参数的&&是万能引用,结合可变参数

emplace 效果的体现场景:主要体现在浅拷贝的拷贝构造的优化

  • emplace 能一直往下传
  • push_back 是先构造,再拷贝构造/移动构造

emplace 可以理解为一个优化的 push_back

5.移动构造和拷贝构造

新的类功能:默认成员函数

在原来的C++类中,编译器会默认生成六个默认成员函数:

  1. 构造函数
  2. 析构函数
  3. 拷贝构造函数
  4. 拷贝赋值运算符重载
  5. 取地址运算符重载
  6. const 取地址运算符重载

其中,最为常用的是前四个,后两个通常使用较少。这些默认成员函数在我们没有显式定义时,编译器会自动生成。

C++11 新增了两个默认成员函数:

  1. 移动构造函数
  2. 移动赋值运算符重载

移动构造函数和移动赋值运算符重载的注意点

  • 如果没有自己定义移动构造函数,并且没有实现析构函数、拷贝构造函数或拷贝赋值运算符中的任意一个,那么编译器会自动生成一个默认的移动构造函数。
    • 对于内置类型成员,执行逐成员按字节的浅拷贝。
    • 对于自定义类型成员,若该成员实现了移动构造函数,则调用移动构造函数;若没有,则调用拷贝构造函数。
  • 类似地,如果没有定义移动赋值运算符重载函数,如上
  • 一旦提供了移动构造函数或移动赋值运算符,编译器就不会再自动生成拷贝构造函数和拷贝赋值运算符。
class Person
{
public:Person(const char* name = "", int age = 0): _name(name), _age(age) {}private:bit::string _name;int _age;
};int main()
{Person s1;Person s2 = s1;            // 调用默认的拷贝构造函数Person s3 = std::move(s1);  // 调用默认的移动构造函数Person s4;s4 = std::move(s2);         // 调用默认的移动赋值运算符return 0;
}

在上述代码中,由于没有定义析构函数、拷贝构造函数和拷贝赋值运算符,编译器会自动生成默认的移动构造和移动赋值运算符。

为什么编译器会自动生成默认的移动构造和移动赋值?

通常,对于需要深拷贝或需要释放资源的类,开发者会显式定义拷贝构造函数、赋值运算符重载和析构函数。如果没有显式定义这些函数,编译器生成的默认移动构造函数和移动赋值运算符将对内置类型执行值拷贝

强制生成 default

在C++11中,可以使用default关键字来强制生成某些默认成员函数,即使开发者提供了其他构造函数。例如,提供了拷贝构造函数后,编译器不会再生成默认的移动构造函数,但可以通过= default来显式请求生成。

class Person
{
public:Person(const char* name = "", int age = 0): _name(name), _age(age) {}Person(const Person& p): _name(p._name), _age(p._age) {}Person(Person&& p) = default;  // 强制生成移动构造函数private:bit::string _name;int _age;
};

即使手动编写了拷贝构造函数,仍然可以通过= default来让编译器生成移动构造函数。

这篇关于[C++11#46](三) 详解lambda | 可变参数模板 | emplace_back | 默认的移动构造的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1136491

相关文章

Debezium 与 Apache Kafka 的集成方式步骤详解

《Debezium与ApacheKafka的集成方式步骤详解》本文详细介绍了如何将Debezium与ApacheKafka集成,包括集成概述、步骤、注意事项等,通过KafkaConnect,D... 目录一、集成概述二、集成步骤1. 准备 Kafka 环境2. 配置 Kafka Connect3. 安装 D

Java中ArrayList和LinkedList有什么区别举例详解

《Java中ArrayList和LinkedList有什么区别举例详解》:本文主要介绍Java中ArrayList和LinkedList区别的相关资料,包括数据结构特性、核心操作性能、内存与GC影... 目录一、底层数据结构二、核心操作性能对比三、内存与 GC 影响四、扩容机制五、线程安全与并发方案六、工程

C++初始化数组的几种常见方法(简单易懂)

《C++初始化数组的几种常见方法(简单易懂)》本文介绍了C++中数组的初始化方法,包括一维数组和二维数组的初始化,以及用new动态初始化数组,在C++11及以上版本中,还提供了使用std::array... 目录1、初始化一维数组1.1、使用列表初始化(推荐方式)1.2、初始化部分列表1.3、使用std::

C++ Primer 多维数组的使用

《C++Primer多维数组的使用》本文主要介绍了多维数组在C++语言中的定义、初始化、下标引用以及使用范围for语句处理多维数组的方法,具有一定的参考价值,感兴趣的可以了解一下... 目录多维数组多维数组的初始化多维数组的下标引用使用范围for语句处理多维数组指针和多维数组多维数组严格来说,C++语言没

Spring Cloud LoadBalancer 负载均衡详解

《SpringCloudLoadBalancer负载均衡详解》本文介绍了如何在SpringCloud中使用SpringCloudLoadBalancer实现客户端负载均衡,并详细讲解了轮询策略和... 目录1. 在 idea 上运行多个服务2. 问题引入3. 负载均衡4. Spring Cloud Load

Springboot中分析SQL性能的两种方式详解

《Springboot中分析SQL性能的两种方式详解》文章介绍了SQL性能分析的两种方式:MyBatis-Plus性能分析插件和p6spy框架,MyBatis-Plus插件配置简单,适用于开发和测试环... 目录SQL性能分析的两种方式:功能介绍实现方式:实现步骤:SQL性能分析的两种方式:功能介绍记录

在 Spring Boot 中使用 @Autowired和 @Bean注解的示例详解

《在SpringBoot中使用@Autowired和@Bean注解的示例详解》本文通过一个示例演示了如何在SpringBoot中使用@Autowired和@Bean注解进行依赖注入和Bean... 目录在 Spring Boot 中使用 @Autowired 和 @Bean 注解示例背景1. 定义 Stud

如何通过海康威视设备网络SDK进行Java二次开发摄像头车牌识别详解

《如何通过海康威视设备网络SDK进行Java二次开发摄像头车牌识别详解》:本文主要介绍如何通过海康威视设备网络SDK进行Java二次开发摄像头车牌识别的相关资料,描述了如何使用海康威视设备网络SD... 目录前言开发流程问题和解决方案dll库加载不到的问题老旧版本sdk不兼容的问题关键实现流程总结前言作为

SQL 中多表查询的常见连接方式详解

《SQL中多表查询的常见连接方式详解》本文介绍SQL中多表查询的常见连接方式,包括内连接(INNERJOIN)、左连接(LEFTJOIN)、右连接(RIGHTJOIN)、全外连接(FULLOUTER... 目录一、连接类型图表(ASCII 形式)二、前置代码(创建示例表)三、连接方式代码示例1. 内连接(I

Go路由注册方法详解

《Go路由注册方法详解》Go语言中,http.NewServeMux()和http.HandleFunc()是两种不同的路由注册方式,前者创建独立的ServeMux实例,适合模块化和分层路由,灵活性高... 目录Go路由注册方法1. 路由注册的方式2. 路由器的独立性3. 灵活性4. 启动服务器的方式5.