C++ 11 --- 中

目录

1. 移动构造和移动赋值的默认形成条件 

2. 强制生成默认函数的关键字 default

3. 禁止生成默认函数的关键字 delete

3.1. 如果要求,用delete实现一个类只能在堆上面构造对象,该如何实现?

4. 可变模板参数

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

4.2. 利用 C++11 的列表初始化展开参数包

4.3. STL 容器中的 emplacae 相关接口 


1. 移动构造和移动赋值的默认形成条件 

在C++11之前,我们认为类的默认成员函数有六个:

构造、析构、拷贝构造、赋值运算符重载、取地址、const取地址,这里就不多解释了。

而在C++11中,新增了两个类的默认成员函数,分别是:移动构造(move constructor),移动赋值(move operator=)。

而我们以前说过,类的默认成员函数,如果我们实现了,编译器不会在自动提供这些默认成员函数;如果我们不写,编译器会自动生成一份,而对于它们俩也不例外,但是,以前的默认成员函数,只要我们不写,编译器就会自动生成一份,例如,我们不写构造,那么编译器就会自动生成一份构造,然而,移动构造和移动赋值自动生成的条件却更为苛刻。

首先,我们应该知道,移动构造和移动赋值的初衷是为了减少拷贝,提高效率。而对于不需要进行深拷贝的类来说,移动构造和移动赋值的价值不大。换句话说,移动构造和移动赋值是为了需要进行深拷贝的类而实现的。因此:

  • 默认移动构造生成的条件是:如果你没有实现移动构造,并且没有实现析构函数和拷贝构造以及赋值运算符重载的任意一个,那么编译器就会自动生成一份默认移动构造函数;
  • 默认生成的移动构造的功能:编译器默认生成的移动构造对内置类型按字节序的方式进行拷贝,对自定义类型会去调用它的移动构造,如果这个自定义类型没有实现移动构造,那么会去调用它的拷贝构造;
  • 默认移动赋值生成的条件:如果你没有实现移动赋值重载函数,且没有实现析构函数和拷贝构造以及赋值运算符重载的任意一个,那么编译器会自动生成一个默认移动赋值;
  • 默认生成的移动赋值的功能:编译器默认生成的移动赋值对内置类型按字节序的方式进行拷贝,对自定义类型会去调用它的移动赋值,如果这个自定义类型没有实现移动赋值,那么会去调用它的赋值运算符重载。

总结,对于一个类来说,如果要写析构,那么说明有资源需要被释放,那么也就意味着需要实现深拷贝和赋值运算符重载。也就是说,看似移动语义的自动生成条件很苛刻,但是如果一个类没有资源需要清理,那么就可以满足移动语义的自动生成条件,反之,如果有资源需要被清理,那么移动语义不会被自动生成。

2. 强制生成默认函数的关键字 default

C++11可以让你更好的控制要使用的默认函数。假设你要使用某个默认的函数,但是因为一些原因这个函数没有默认生成。比如:我们提供了拷贝构造,就不会生成移动构造了,那么我们可以使用 default 关键字显示指定移动构造生成。

class B
{
public:
	B(int b = 0) :_b(b) {}
	B(const B& b)
		:_b(b._b)
	{
		std::cout << " B(const B& b) " << std::endl;
	}
	// 由于此时已经实现了拷贝构造,编译器不会默认生成移动构造
    // 因此我们可以强制生成移动构造
	B(B&& ref) = default;
private:
	int _b = 0;
};

3. 禁止生成默认函数的关键字 delete

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

class A
{
public:
	A(int a = 0) :_a(a) {}

	// 假如我不想让别人通过拷贝我以构造一个新对象
	A(const A& a) = delete;
private:
	int _a = 0;
};
void Test1(void)
{
	A a;
    // 编译报错,此时就无法调用拷贝构造函数
	A copy(a);
}

3.1. 如果要求,用delete实现一个类只能在堆上面构造对象,该如何实现?

class heap_only
{
public:
	// 禁止生成析构函数
	~heap_only() = delete;
};
void Test3(void)
{
    // 下面的两种实例化对象的方式都会报错,因为此时没有析构函数,无法创建对象
	heap_only ho;  
	static heap_only s_ho;
    // 那此时如何创建对象呢?
    // 此时可以new一个对象
    heap_only* new_ho = new heap_only;
}

上面的问题解决了,那假如此时这个类有资源需要被清理,该如何解决?

class heap_only
{
public:
	heap_only()
		:_str(new char[10])
	{}
	// 禁止生成析构函数
	~heap_only() = delete;

    void destroy()
	{
        // "内层"资源:
		delete[] _str;
        // "外层"资源
        operator delete(this);
	} 
private:
	char* _str = nullptr;
};

void Test3(void)
{
    // 因为此时析构无法调用,那么如何释放资源呢?
	heap_only* new_ho = new heap_only;
    //delete[] new_ho;  // 报错,无法调用析构
    // 但是,此时我可以调用一个destroy,释放这些资源
    new_ho->destroy();
}

4. 可变模板参数

可变参数(variadic arguments)是指在函数或模板中可以接受任意数量的参数。C++中有两种方式来实现可变参数:函数的可变参数和模板的可变参数

参数包(parameter pack)是C++中的一个特性,它允许在模板中一次传递多个参数。参数包可以包含任意数量的参数,包括类型、非类型和模板参数。

参数包用三个点号(...)表示,它里面包含了0到N(N>=0)个参数。

下面是一个可变参数的函数模板,如何使用递归展开这个参数包?

我们无法直接获取参数包 args 中的每个参数的, 只能通过展开参数包的方式来获取参数包中的每个参数

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

// 递归终止函数
void print()
{
	std::cout << std::endl;
}

template<class ...Args>
void get_size(Args... args)
{
    // 在这里可以用sizeof计算当前的参数个数  sizeof...(参数包)
    std::cout << sizeof...(args) << std::endl;
}

// Args是一个模板参数包,args是一个函数形参参数包
// 声明一个函数参数包Args... args,这个参数包中可以包含0到任意个模板参数。
// 参数包展开函数
// 这个递归过程是一个编译时决议,编译过程中推args这个参数包有几个参数
template<class T,class ...Args>
void print(const T& t,Args... args)
{
	std::cout << t << std::endl;
	print(args...);
}

void Test4(void)
{
	print(1);
	print(1,'x');
	print(1, 'x', std::string("haha"));
}

4.2. 利用 C++11 的列表初始化展开参数包

在这里利用了C++11的一个特性 --- 列表初始化,通过列表初始化来初始化一个变长数组, {(printarg(args), 0)...} 将会展开成{ printarg(arg0), printarg(arg1), printarg(arg2), ... printarg(argN) },最终会创建一个元素值都为0的数组 int arr[sizeof... (args)]的数组

template<class T>
int print_arg(const T& t)
{
	std::cout << t << std::endl;
	// 这里的返回值: 用于初始化数组
	return 0; 
}

template<class ...Args>
void print(Args... args)
{
	// 这是一个列表初始化,用于初始化一个变长数组
	// { print_arg(args)...} 将会被展开 { print_arg(args0) }, { print_arg(args1) } , ... , { print_arg(argsN) }
	//这个数组的目的就是单纯为了在数组构造的过程展开参数包
	int a[] = { print_arg(args)...};
}

void Test4(void)
{
	print(1);
	print(1,'x');
	print(1, 'x', std::string("haha"));
}

4.3. STL 容器中的 emplacae 相关接口 

如下:

emplace 的相关接口还有很多,在这就不一一举例了, 首先我们看到的 emplace 系列的接口,是支持模板的可变参数,并且万能引用。

我们在这里以list举例,push_back和emplace系列接口的优势到底在哪里呢?

class Time
{
public:
	Time(int hours, int minute, int second)
		:_hours(hours)
		, _minute(minute)
		, _second(second)
	{std::cout << "Time(int hours, int minute, int second)" << std::endl;}

	Time(const Time& copy)
	{
		std::cout << "Time(const Time& copy)" << std::endl;
	}

	Time& operator=(const Time& copy)
	{
		std::cout << "Time& operator=(const Time& copy)" << std::endl;
	}

private:
	int _hours = 0;
	int _minute = 0;
	int _second = 0;
};

void Test5(void)
{
	std::list<Time> lt;
    // push_back()在这里会先构造Time这个匿名对象;
    // 如果此时实现了移动构造,那么就调用移动构造
    // 如果没实现移动构造,那么就调用拷贝构造
	lt.push_back(Time(8, 0, 0));
	std::cout << " ------------------------ " << std::endl;
    // 而emplace_back() 在这里直接构造Time()
    // 与push_back()相比,少了一次移动构造或者拷贝构造
	lt.emplace_back(12, 0, 0);
}

现象如下:

push_back:是将给定元素以副本的方式添加到容器的末尾。它接受一个参数,该参数会被复制(或者移动)到容器中。

emplace_back():是在容器的末尾直接创建对象,而不是通过复制或移动已有对象。它接受任意数量的参数,并将这些参数传递给对象的构造函数在容器中就地构造元素。这样可以避免额外的复制或移动操作,提高插入操作的效率。

总结:

  • push_back() 需要提供已有类型的对象,会进行一次复制或移动操作;
  • emplace_back()可以直接在容器中就地构造元素,避免了额外的复制或移动操作,可以提高效率;
  • emplace_back()的参数会被传递给元素的构造函数,因此可以接受任意数量和类型的参数;
  • 尽可能使用emplace_back()来避免不必要的对象复制或移动,以提高性能和代码效率。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值