C++11之可变参数模板

C++11

    • 一、可变参数模板的基本概念
    • 二.参数包扩展
      • 递归包展开
        • 参数包展开的原理
          • 参数包的定义
          • 参数包的展开
      • 另类包展开
        • 参数包展开的原理
        • 参数包的定义
        • 参数包的展开
  • 三.emplace系列接口
    • 3.1emplace出现的原因
    • 3.2`emplace` 系列接口的登场
    • 3.3`emplace` 系列接口的原理
    • 3.4通过代码来看一看emplace的效率
  • 四.新的类功能
    • 4.1 默认的移动构造和移动赋值
    • 4.2成员变量声明时给缺省值
    • 4.3defult和delete
    • 4.4final与override
      • 1. **final 关键字**
      • 2. **override 关键字**

C++11之可变参数模板_第1张图片

在 C++ 的世界里,模板机制一直是提升代码复用性和泛型编程能力的重要工具。而可变参数模板(Variadic Templates)作为 C++11 引入的一项强大特性,更是将模板的灵活性推向了新的高度。它允许我们编写能够处理任意数量、任意类型参数的函数和类,极大地增强了代码的通用性和适应性。

一、可变参数模板的基本概念

在传统 C++ 中,函数的参数数量和类型是固定的,这在很多情况下限制了函数的通用性。而可变参数模板的出现,打破了这一限制。它允许函数接受不确定数量的参数,这些参数可以是任意类型,从而让函数能够以更加灵活的方式处理各种不同的输入。

可变参数模板的核心是使用 ...(三个点)来表示参数包(parameter pack)。参数包可以看作是一个包含了多个参数的集合,这些参数在函数中可以被逐一处理。例如,以下是一个简单的可变参数模板函数的声明:

template<typename... Args>
void print(Args... args);

在这个例子中,Args... args 就是一个参数,包Args 是一个模板参数列表,它代表了参数的类型,而 args 是参数包的名称。这个函数可以接受任意数量和类型的参数,这些参数在函数体内将被统一处理。

二.参数包扩展

递归包展开

void ShowList()
{
	cout << endl;
}

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

template <class ...Args>
void Print(Args... args)
{
	// N个参数,第一个传给x,剩下N-1参数传给ShowList的第二个参数包
	ShowList(args...);
}

这段代码实现了一个递归打印的功能。Print 函数接收一个参数包 args,然后通过 ShowList(args...) 的方式将参数包展开,并将展开后的结果传递给 ShowList 函数。ShowList 函数通过递归的方式依次打印每个参数,直到参数包为空。

参数包展开的原理
参数包的定义

在我们的示例中,template 定义了一个参数包 Args,它表示 Print 函数可以接受任意数量和类型的参数。

参数包的展开

参数包展开是指将参数包中的每个参数依次展开,以便在代码中对每个参数进行操作。展开操作是通过 ... 运算符来完成的。

Print 函数中,ShowList(args...) 是参数包展开的关键。这里,args 是参数包,ShowList(args...) 的作用是将参数包中的每个参数依次传递给 ShowList 函数。

例如,如果调用 Print(1, 2.5, "hello"),那么参数包 args 包含三个参数:12.5"hello"ShowList(args...) 的展开过程如下:

  • 第一次调用 ShowList(1, 2.5, "hello")Tintx1,剩下的参数包 args2.5"hello"
  • 第二次调用 ShowList(2.5, "hello")Tdoublex2.5,剩下的参数包 args"hello"
  • 第三次调用 ShowList("hello")Tconst char*x"hello",剩下的参数包 args 是空的。
  • 最后调用 ShowList(),参数包为空,打印换行符并结束递归。

另类包展开

首先,让我们来看一下本文的核心代码示例:

const T& GetArg(const T& x)
{
	cout << x << " ";
	return x;
}

template <class ...Args>
void Arguments(Args... args)
{}

template <class ...Args>
void Print(Args... args)
{
	// 注意GetArg必须返回或者到的对象,这样才能组成参数包给Arguments
	// GetArg的返回值组成实参参数包,传给Arguments
	Arguments(GetArg(args)...);
}

这段代码中,Print 函数是关键。它接收一个参数包 args,然后通过 GetArg(args)... 的方式将参数包展开,并将展开后的结果作为参数传递给 Arguments 函数。

参数包展开的原理
参数包的定义

template 定义了一个参数包 Args,它表示 Print 函数可以接受任意数量和类型的参数。

参数包的展开

参数包展开是指将参数包中的每个参数依次展开,以便在代码中对每个参数进行操作。展开操作是通过 ... 运算符来完成的。

Print 函数中,GetArg(args)... 是参数包展开的关键。这里,args 是参数包,GetArg(args) 表示对参数包中的每个参数调用 GetArg 函数。... 运算符的作用是将 GetArg(args) 对每个参数的调用结果展开成一个参数列表,然后将这个参数列表传递给 Arguments 函数。

例如,如果调用 Print(1, 2.5, "hello"),那么参数包 args 包含三个参数:12.5"hello"GetArg(args)... 的展开过程如下:

  • 对第一个参数 1 调用 GetArg(1),得到返回值 1
  • 对第二个参数 2.5 调用 GetArg(2.5),得到返回值 2.5
  • 对第三个参数 "hello" 调用 GetArg("hello"),得到返回值 "hello"

最终,GetArg(args)... 展开成 GetArg(1), GetArg(2.5), GetArg("hello"),这相当于 1, 2.5, "hello",然后将这个参数列表传递给 Arguments 函数。

三.emplace系列接口

template <class... Args> void emplace_back (Args&&... args);

template <class... Args> iterator emplace (const_iterator position, Args&&... args);
  1. C++11以后STL容器新增了empalce系列的接⼝,empalce系列的接⼝均为模板可变参数,功能上兼容push和insert系列,但是empalce还⽀持新玩法,假设容器为container,empalce还⽀持直接插⼊构造T对象的参数,这样有些场景会更⾼效⼀些,可以直接在容器空间上构造T对象。

  2. emplace_back总体⽽⾔是更⾼效,推荐以后使⽤emplace系列替代insert和push系列

  3. 传递参数包过程中,如果是 Args&&… args 的参数包,要⽤完美转发参数包,⽅式如下std::forward(args)… ,否则编译时包扩展后右值引⽤变量表达式就变成了左值。

3.1emplace出现的原因

emplace 出现之前,我们往容器里添加元素时,常常会遇到一些效率问题。比如,当我们使用 std::vectorstd::map 等容器时,如果要添加一个对象,通常的做法是先构造好对象,然后再将其拷贝或移动到容器中。这个过程涉及到临时对象的创建、拷贝或移动构造等操作,不仅代码显得繁琐,还可能带来不必要的性能开销,尤其是在处理大量数据或复杂对象时,这种开销会更加明显。

3.2emplace 系列接口的登场

C++11 引入了 emplace 系列接口,为容器操作带来了革命性的变化。emplace 的基本思想是在容器分配的存储空间上直接构造对象,从而避免了临时对象的创建和拷贝/移动构造等中间步骤。

std::vector 为例,其 emplace_back 方法允许我们在向量的末尾直接构造一个对象。假设我们有一个类 Person,包含姓名和年龄两个成员变量,我们想往 std::vector 中添加一个 Person 对象。传统的做法可能是这样:

std::vector<Person> people;
Person p("Alice", 25);
people.push_back(p);

这里,Person 对象 p 先被构造出来,然后通过 push_back 方法拷贝到 std::vector 中,涉及到两次构造(一次是 p 的构造,一次是拷贝构造到向量中)。

而使用 emplace_back,我们可以这样写:

std::vector<Person> people;
people.emplace_back("Alice", 25);

在这个例子中,emplace_back 直接在 std::vector 分配的存储空间上构造了一个 Person 对象,构造参数直接传递给 Person 的构造函数。这样,就避免了临时对象 p 的创建以及后续的拷贝构造,大大提高了效率。

3.3emplace 系列接口的原理

emplace 系列接口的魔法在于它利用了完美转发和变长参数模板。当调用 emplaceemplace_back 等方法时,这些方法会将传入的参数完美转发给容器中元素类型的构造函数,从而在容器分配的内存空间上直接构造对象。

std::mapemplace 方法为例,其大致实现原理如下:

template <typename Key, typename T, typename Compare, typename Allocator>
template <typename... Args>
std::pair<typename std::map<Key, T, Compare, Allocator>::iterator, bool>
std::map<Key, T, Compare, Allocator>::emplace(Args&&... args)
{
    // 在合适的位置分配内存
    auto position = ...;
    // 完美转发参数给键值对的构造函数,在分配的内存上直接构造对象
    auto result = emplace_hint(position, std::forward<Args>(args)...);
    return result;
}

这里的关键是 std::forward(args)...,它利用完美转发将参数原封不动地转发给元素类型的构造函数,从而保证了参数的值类别(左值或右值)不会改变,使得构造函数能够根据参数的实际类型进行正确的构造。

3.4通过代码来看一看emplace的效率

int main()
{
	// 效率用法都是一样的
	bit::list<int> lt1;
	lt1.push_back(1);
	lt1.emplace_back(2);

	bit::list<bit::string> lt2;
	// 传左值效率用法都是一样的
	bit::string s1("111111111");
	lt2.push_back(s1);
	lt2.emplace_back(s1);
	cout << "*****************************************" << endl;
	// 传右值效率用法都是一样的
	bit::string s2("111111111");
	lt2.push_back(move(s2));
	bit::string s3("111111111");
	lt2.emplace_back(move(s3));
	cout << "*****************************************" << endl;
	// emplace_back的效率略高一筹
	lt2.push_back("1111111111111111111111111111");
	lt2.emplace_back("11111111111111111111111111");
	cout << "*****************************************" << endl;
}

四.新的类功能

4.1 默认的移动构造和移动赋值

  1. 原来C++类中,有6个默认成员函数:构造函数/析构函数/拷⻉构造函数/拷⻉赋值重载/取地址重载/const取地址重载,最后重要的是前4个,后两个⽤处不⼤,默认成员函数就是我们不写编译器会⽣成⼀个默认的。C++11新增了两个默认成员函数,移动构造函数和移动赋值运算符重载。

  2. 如果你没有⾃⼰实现移动构造函数,且没有实现析构函数、拷⻉构造、拷⻉赋值重载中的任意⼀个。那么编译器会⾃动⽣成⼀个默认移动构造。默认⽣成的移动构造函数,对于内置类型成员会执⾏逐成员按字节拷⻉,⾃定义类型成员,则需要看这个成员是否实现移动构造,如果实现了就调⽤移动构造,没有实现就调⽤拷⻉构造。

  3. 如果你没有⾃⼰实现移动赋值重载函数,且没有实现析构函数、拷⻉构造、拷⻉赋值重载中的任意⼀个,那么编译器会⾃动⽣成⼀个默认移动赋值。默认⽣成的移动构造函数,对于内置类型成员会执⾏逐成员按字节拷⻉,⾃定义类型成员,则需要看这个成员是否实现移动赋值,如果实现了就调⽤移动赋值,没有实现就调⽤拷⻉赋值。(默认移动赋值跟上⾯移动构造完全类似)

  4. 如果你提供了移动构造或者移动赋值,编译器不会⾃动提供拷⻉构造和拷⻉赋值。

4.2成员变量声明时给缺省值

成员变量声明时给缺省值是给初始化列表⽤的,如果没有显⽰在初始化列表初始化,就会在初始化列表⽤这个缺省值初始化

4.3defult和delete

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

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

4.4final与override

1. final 关键字

  • 修饰类:当一个类被final修饰时,意味着这个类不能被继承。
    class Base final { /* ... */ };
    class Derived : public Base { /* 错误:Base 是 final 类,不能被继承 */ };
    
  • 修饰虚函数:若虚函数被final修饰,那么在派生类中就不能再对该函数进行重写。
    class Base {
    public:
        virtual void func() final; // 声明为 final
    };
    
    class Derived : public Base {
    public:
        void func(); // 错误:不能重写 final 函数
    };
    

2. override 关键字

在C++里,override用于显式表明派生类中的函数是对基类虚函数的重写。如果使用了override却没有成功重写基类虚函数,编译器就会报错。

class Base {
public:
    virtual void func(int x);
};

class Derived : public Base {
public:
    void func(int x) override; // 正确:重写基类虚函数
    void func(double x) override; // 错误:基类中没有匹配的虚函数
};

C++11之可变参数模板_第2张图片

你可能感兴趣的:(C++11之可变参数模板)