12.C++11——新的类功能&&可变参数模版&&lambda表达式

新的类功能

移动构造和移动赋值

原来C++类中,有6个默认成员函数:构造函数、拷贝构造函数、拷贝赋值重载、析构函数、取地址重载、const取地址重载。最后重要的是前4个,后两个用处不大。默认成员函数就是我们不写编译器会生成一个默认的。 C++11 新增了两个:移动构造函数和移动赋值重载

  • 移动构造函数

    如果你没有自己实现移动构造函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个,那么编译器会自动生成一个默认移动构造。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动构造,如果实现了就调用移动构造,没有实现就调用拷贝构造

  • 移动赋值重载

    如果你没有自己实现移动赋值重载函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个,那么编译器会自动生成一个默认移动赋值。默认生成的移动赋值重载,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动赋值,如果实现了就调用移动赋值,没有实现就调用拷贝赋值。(默认移动赋值跟上面移动构造完全类似)

移动构造和移动赋值的生成条件较为严格,若类中显式定义了析构函数、拷贝构造或赋值重载,则编译器不会自动生成。这是因为析构、拷贝构造和赋值重载通常是一体化的,若需要显式定义析构函数,说明类中有资源需要释放,此时默认生成的拷贝构造可能无法满足需求。因此,若需要深拷贝,通常需要显式定义拷贝构造和赋值重载,同时也需要显式定义移动构造和移动赋值,以便在右值情况下转移资源。

对于浅拷贝的类,不需要定义析构、拷贝构造或赋值重载,因此编译器生成的移动构造也无意义。但对于成员可能包含自定义类型的类,若自定义类型有移动构造,则编译器生成的移动构造会调用其移动构造,提高效率。自动生成的移动构造主要是为这类自身无资源管理但成员有资源需求的类准备的。

关键字default

C++11可以让你更好的控制要使用的默认函数。假设你要使用某个默认的函数,但是因为一些原因这个函数没有默认生成。比如:我们提供了拷贝构造,就不会生成移动构造了,那么我们可以使用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;
};
关键字delete

如果能想要限制某些默认函数的生成,在C++98中,是该函数设置成private,并且只声明补丁已,这样只要其他人想要调用就会报错。在C++11中更简单,只需在该函数声明加上=delete即可,该语法指示编译器不生成对应函数的默认版本,称=delete修饰的函数为删除函数。

class Person
{
public:
	Person(const char* name = "111111111111", int age = 0)
		:_name(name)
		, _age(age)
	{}

    // 只声明不实现,声明为私有
    // C++98
    //private:
    //	Person(const Person& p);
    //	Person& operator=(const Person & p);

	Person(const Person& p) = delete;
	Person& operator=(const Person& p) = delete;
private:
	bit::string _name;
	int _age;
};

可变参数模版

PS: 这一部分都是课件上的内容,了解即可

C++11的新特性可变参数模板能够让您创建可以接受可变参数的函数模板和类模板,相比C++98/03,类模版和函数模版中只能含固定数量的模版参数,可变模版参数无疑是一个巨大的改进。

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

上面的参数args前面有省略号,所以它就是一个可变模版参数,我们把带省略号的参数称为“参数包”,它里面包含了0到N(N>=0)个模版参数。我们无法直接获取参数包args中的每个参数的,只能通过展开参数包的方式来获取参数包中的每个参数,这是使用可变模版参数的一个主要特点,也是最大的难点,即如何展开可变模版参数。由于语法不支持使用args[i]这样方式获取可变参数,所以我们的用一些奇招来一一获取参数包的值。

递归函数方式展开参数包

// 递归终止函数
template <class T>
void ShowList(const T& t)
{
	cout << t << endl;
}
// 展开函数
template <class T, class ...Args>
void ShowList(T value, Args... args)
{
	cout << value << " ";
	ShowList(args...);
}
int main()
{
	ShowList(1);
	ShowList(1, 'A');
	ShowList(1, 'A', std::string("sort"));
	return 0;
}

逗号表达式展开参数包

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);
	ShowList(1, 'A');
	ShowList(1, 'A', std::string("sort"));
	return 0;
}

​ 这种展开参数包的方式,不需要通过递归终止函数,是直接在expand函数体中展开的, printarg不是一个递归终止函数,只是一个处理参数包中每一个参数的函数。这种就地展开参数包的方式实现的关键是逗号表达式。我们知道逗号表达式会按顺序执行逗号前面的表达式。

expand函数中的逗号表达式:(printarg(args), 0),也是按照这个执行顺序,先执行printarg(args),再得到逗号表达式的结果0。同时还用到了C++11的另外一个特性——初始化列表,通过初始化列表来初始化一个变长数组, {(printarg(args), 0)...}将会展开成((printarg(arg1),0), (printarg(arg2),0), (printarg(arg3),0), etc... ),最终会创建一个元素值都为0的数组int arr[sizeof...(Args)]。由于是逗号表达式,在创建数组的过程中会先执行逗号表达式前面的部分printarg(args)打印出参数,也就是说在构造int数组的过程中就将参数包展开了,这个数组的目的纯粹是为了在数组构造的过程展开参数包

在实际使用时,我们不一定要展开参数包,比如STL容器中的empalce相关接口函数,只需要将参数包向下传递即可。

lambda表达式

引入:

  • 在实际编程中,对简单整型数组排序的场景较少,更多的是对结构体或类对象数组的排序。例如,在电商网站中,商品可以按价格、销量、评价等多种方式排序。在学校考试系统中,学生可以按总分或单科成绩排序。这些排序需求要求对复合对象的特定属性进行比较。
  • 传统的做法是通过重载运算符或使用仿函数来实现比较逻辑。然而,重载运算符只能固定一种比较方式,缺乏灵活性。仿函数可以提供多种比较方式,例如按价格升序、降序或按评价排序。但为每种比较方式编写单独的仿函数会导致代码冗余。在C++中,使用仿函数进行排序需要传递函数对象,例如在sort算法中。虽然仿函数提供了灵活性,但编写和维护大量仿函数仍然繁琐。这种场景下,需要更简洁、更灵活的解决方案来处理多样化的排序需求

C++11引入了lambda表达式,这是一种从其他语言借鉴的特性,旨在简化函数对象的创建和使用。lambda表达式特别适用于需要临时函数对象的场景,如排序算法中的比较操作。与传统的仿函数相比,lambda表达式更加简洁,可以直接在调用处定义,无需预先声明单独的类。

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

  • lambda表达式的语法结构由五个部分组成,包括捕捉列表[ ]、参数( )、可变关键字mutable 、返回值-> return-type和函数体statement

    • 捕捉列表使用方括号括起来,参数部分与普通函数参数写法相同。编译器根据[]来判断接下来的代码是否为lambda函数,捕捉列表能够捕捉上下文中的变量供lambda函数使用
    • 参数列表,与普通函数的参数列表一致
    • 可变关键字平时很少使用
    • 返回值部分采用箭头加返回类型的格式
    • 函数体部分包含具体实现代码
  • lambda表达式也可以叫匿名函数,与普通函数的主要区别在于没有函数名。虽然语法结构包含五个部分,但实际核心部分只有四个,因为可变关键字在大多数情况下不需要使用。

  • 匿名函数可以简写,当没有参数时可以省略参数列表及其(),但如果写了可变关键字就不能省略参数列表;当返回值明确或没有返回值时可以省略返回值部分。

  • 匿名函数的返回值需要通过auto关键字接收,因为它没有固定类型。匿名函数对象可以像普通函数一样被调用。但是要注意:只能用auto来接收,即便在lambda表达式中明确写出了返回值类型

int main()
{
	auto add1 = [](int x, int y)->int {return x + y; };
	cout << add1(1, 2) << endl;

	auto func1 = []()->int
	{
		cout << "hello bit" << endl;
		cout << "hello world" << endl;
		return 0;
	};

	func1();

	// 返回值类型可自动推导类型,所以可以省略
	// 无参数可以省略
	auto func2 = []
	{
		cout << "hello bit" << endl;
		cout << "hello world" << endl;
		return 0;
	};

	cout << func2() << endl;

	auto func3 = []
	{
		cout << "hello bit" << endl;
		cout << "hello world" << endl;
	};

	func3();

	return 0;
}

运行结果:

3
hello bit
hello world
hello bit
hello world
0
hello bit
hello world
  • 调试Lambda表达式时,应避免在Lambda表达式内部设置断点,因为这会使得调试过程变得复杂和难以追踪。
使用细节
  • 捕捉列表[ ]

    • 表用于捕获外部变量,分为传值捕捉和传引用捕捉。传值捕捉会创建变量的拷贝,而传引用捕捉则直接引用外部变量。

    • 在捕捉列表中,可以指定需要捕获的变量,多个变量用逗号分隔。

    • 默认情况下,传值捕捉的变量是不可修改的,需要通过mutable关键字来允许修改。但是只会修改lambda表达式中的 传值捕捉的变量,传引用捕捉则会修改lambda表达式之外的值

      int main()
      {
      	int a = 0, b = 1;
      
      	auto swap1 = [](int& x, int& y)
      		{
      			// 只能用当前lambda局部域和捕捉的对象
      			int tmp = x;
      			x = y;
      			y = tmp;
      		};
      
      	swap1(a, b);
      	std::cout << "a: " << a << " b: " << b << std::endl;
      
      	// 传值捕捉本质是一种拷贝,并且const修饰了
      	// mutable相当于去掉const属性,可以修改了
      	// 但是修改了不会影响外面被捕捉的值,因为是一种拷贝
      	auto swap2 = [a, b]()mutable
      		{
      			int tmp = a;
      			a = b;
      			b = tmp;
      		};
      
      	swap2();
      	std::cout << "a: " << a << " b: " << b << std::endl;
      
      	auto swap3 = [&a, &b]()
      		{
      			int tmp = a;
      			a = b;
      			b = tmp;
      		};
      
      	swap3();
      	std::cout << "a: " << a << " : " << b << std::endl;
      
      	return 0;
      }
      

      运行结果:

      a: 1 b: 0
      a: 1 b: 0
      a: 0 b: 1
      
    • 其他使用方法

      [var]:表示值传递方式捕捉变量var

      [=]:表示值传递方式捕获所有父作用域中的变量(包括this)

      [&var]:表示引用传递捕捉变量var

      [&]:表示引用传递捕捉所有父作用域中的变量(包括this)

      [this]:表示值传递方式捕捉当前的this指针

    int main()
    {
    	// 只能用当前lambda局部域和捕捉的对象和全局对象
    
    	int a = 0, b = 1, c = 2, d = 3;
    	// 所有值传值捕捉
    	auto func1 = [=]
    	{
    		int ret = a + b + c + d + x;
    		return ret;
    	};
    	
    	// 所有值传引用捕捉
    	auto func2 = [&]
    	{
    		a++;
    		b++;
    		c++;
    		d++;
    		int ret = a + b + c + d;
    		return ret;
    	};
    
    	// 混合捕捉
    	auto func3 = [&a, b]
    	{
    		a++;
    		// b++;
    		int ret = a + b;
    		return ret;
    	};
    
    	// 混合捕捉
    	// 所有值以引用方式捕捉,d用传值捕捉
    	auto func4 = [&, d]
    	{
    		a++;
    		b++;
    		c++;
    		//d++;
    
    		int ret = a + b + c + d;
    	};
    
    	auto func5 = [=, &d]() mutable
    	{
    		a++;
    		b++;
    		c++;
    		d++;
    		int ret = a + b + c + d;
    	};
    
    	return 0;
    }
    

包装器

包装器是一种用于封装可调用对象的工具,主要包括function和bind两种类型。

function

function能够封装各种可调用对象,本质上是一个仿函数,使对象能够像函数一样被调用。它可以包装函数指针、仿函数和lambda表达式三类可调用对象。

std::function<ReturnType(ParamType1, ParamType2, ...)>

使用举例:

#include<functional>

int f(int a, int b)
{
	return a + b;
}

struct Functor
{
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()
{
	// 包装可调用对象
	function<int(int, int)> f1 = f;										//函数指针
	function<int(int, int)> f2 = Functor();								//仿函数,注意带上()
	function<int(int, int)> f3 = [](int a, int b) {return a + b; };		//包装lambda表达式

	cout << f1(1, 1) << endl;
	cout << f2(1, 1) << endl;
	cout << f3(1, 1) << endl;

	// 包装静态成员函数
	function<int(int, int)> f4 = &Plus::plusi;							//包装静态成员函数要指定类域并且需要取地址
	cout << f4(1, 1) << endl;

	// 包装非静态成员函数
	function<double(Plus*, double, double)> f5 = &Plus::plusd;			//包装非静态的成员函数时,成员函数会有一个隐含的this指针参数,可																			 以通过定义一个临时对象解决
	Plus pd;
	cout << f5(&pd, 1.1, 1.1) << endl;

	function<double(Plus, double, double)> f6 = &Plus::plusd;
	cout << f6(pd, 1.1, 1.1) << endl;
	cout << f6(Plus(), 1.1, 1.1) << endl;

	return 0;
}

注意:

  1. 包装仿函数,注意带上()
  2. 包装静态成员函数要指定类域并且需要取地址
  3. 包装非静态的成员函数时,成员函数会有一个隐含的this指针参数,导致直接包装时类型不匹配。两种解决方案:第一种是显式传递this指针作为参数,在调用时传入对象地址;第二种是直接传递对象本身(可以是有名对象或匿名对象,不是指针)。为什么第二种方案可行:function包装器内部并不是直接调用成员函数,而是通过operator()间接调用。当传递对象时,包装器会在调用时正确处理this指针。
bind

std::bind函数定义在头文件中,是一个函数模板,它就像一个函数包装器(适配器),接受一个可调用对象(callable object),生成一个新的可调用对象来“适应”原对象的参数列表。一般而言,我们用它可以把一个原本接收N个参数的函数fn,通过绑定一些参数,返回一个接收M个(M可以大于N,但这么做没什么意义)参数的新函数。同时,使用std::bind函数还可以实现参数顺序调整等操作

  • 绑定的主要作用是解决重复传递相同参数的问题。比如原本需要三个参数,通过绑定可以固定某些参数,调用时只需传递剩余参数。此外,bind还可以用于参数顺序的调整

  • template <class Fn, class... Args>
      /* unspecified */ bind (Fn&& fn, Args&&... args);
    

    第一个参数是一个函数对象,后面的可变参数列表是需要绑定的值或者是placeholders的成员。placeholders这个命名空间声明了未指定数量的对象:_1,_2,_3,…,用于在调用bind函数时指定占位符

  • 使用举例:

    using placeholders::_1;
    using placeholders::_2;
    using placeholders::_3;
    
    int Sub(int a, int b)
    {
    	return (a - b) * 10;
    }
    
    int SubX(int a, int b, int c)
    {
    	return (a - b - c) * 10;
    }
    
    int main()
    {
    	auto sub1 = bind(Sub, _1, _2);
    	cout << sub1(10, 5) << endl;
    	//结果:50
        
    	// bind 本质返回的一个仿函数对象
    	// 调整参数顺序(不常用)
    	// _1代表第一个实参
    	// _2代表第二个实参
    	// ...
    	auto sub2 = bind(Sub, _2, _1);
    	cout << sub2(10, 5) << endl;
        //结果:-50
        
    
    	// 调整参数个数 (常用)
    	auto sub3 = bind(Sub, 100, _1);
    	cout << sub3(5) << endl;
        //结果:950
    
    	auto sub4 = bind(Sub, _1, 100);
    	cout << sub4(5) << endl;
        //结果:-950
    
    	// 分别绑死第123个参数
    	auto sub5 = bind(SubX, 100, _1, _2);
    	cout << sub5(5, 1) << endl;
        //结果:940
    
    	auto sub6 = bind(SubX, _1, 100, _2);
    	cout << sub6(5, 1) << endl;
        //结果:-960
    
    	auto sub7 = bind(SubX, _1, _2, 100);
    	cout << sub7(5, 1) << endl;
        //结果:-960
    
    	// bind一般用于,绑死一些固定参数
    	function<double(double, double)> f7 = bind(&Plus::plusd, Plus(), _1, _2);
    	cout << f7(1.1, 1.1) << endl;
        //结果:2.2
    
    	return 0;
    }
    

    具体解析:

    在这里插入图片描述

Logo

码道开发者社区,聚焦华为云码道 CodeArts 代码智能体,沉淀 Agent、Skill、鸿蒙开发实战内容,供开发者查阅资料、交流技术、分享工程实践

更多推荐