【C++11】initializer_list、可变参数模板详解

目录

一、统一的列表初始化

1.{}初始化

在C++98标准中,允许使用花括号{}对数组或者结构体元素进行统一的列表初始值设定

cpp 复制代码
int array1[] = { 1, 2, 3, 4, 5 };
cpp 复制代码
struct Point
{
 int _x;
 int _y;
};
Point p = { 1, 2 };

C++11扩大了用大括号括起的列表(初始化列表)的使用范围

使其可用于所有的内置类型和用户自定义的类型,使用初始化列表时,可添加等号(=),也可不添加

创建对象时也可以使用列表初始化方式调用构造函数初始化

cpp 复制代码
struct Point
{
    int _x;
    int _y;
};

class Date
{
public:
    Date(int year, int month, int day)
        :_year(year)
        , _month(month)
        , _day(day)
    {
        cout << "Date(int year, int month, int day)" << endl;
    }
private:
    int _year;
    int _month;
    int _day;
};

int main()
{
    int x1 = 1;
    int x2{ 2 };
    int array1[]{ 1, 2, 3, 4, 5 };
    int array2[5]{ 0 };
    //结构体
    Point p{ 1, 2 };

    //new表达式
    int* pa = new int[4] { 0 };

    //类
    Date d1(2022, 1, 1);
    // C++11支持列表初始化,这里会调用构造函数初始化
    Date d2{ 2022, 1, 2 };
    Date d3 = { 2022, 1, 3 };
    return 0;
}

总结一句话就是,C++11想让"一切皆可{}初始化"

2.initializer_list

在C++11中,std::initializer_list 是一个模板类,它提供了一种方式来初始化对象或容器,使得代码更加直观和易于书写。

std::initializer_list 允许你使用花括号 {} 包围的列表来初始化对象

这种方式与数组的初始化方式相似,但更加灵活,因为它可以用于任何类型的对象,包括自定义类型

std::initializer_list 的定义位于头文件 <initializer_list> 中。

它通常作为构造函数的一个参数,用于接收一个初始化列表。

C++11对STL中的不少容器就增加std::initializer_list作为参数的构造函数,这样初始化容器对象就更方便了。也可以作为operator=的参数,这样就可以用大括号赋值。

cpp 复制代码
int main()
{
    vector<int> v = { 1,2,3,4 };
    list<int> lt = { 1,2 };
    // 这里{"sort", "排序"}会先初始化构造一个pair对象
    map<string, string> dict = { {"sort", "排序"}, {"insert", "插入"} };
    // 使用大括号对容器赋值
    v = { 10, 20, 30 };
    return 0;
}

下面是一个简单的例子

展示了如何定义一个接受std::initializer_list 的构造函数,并在创建对象时使用它:

cpp 复制代码
#include <iostream>  
#include <initializer_list>  
#include <vector>  
  
class MyClass {  
public:  
    std::vector<int> values;  
  
    // 构造函数,接受一个 initializer_list<int>  
    MyClass(std::initializer_list<int> init_list) : values(init_list) {}  
  
    void print() {  
        for (int val : values) {  
            std::cout << val << " ";  
        }  
        std::cout << std::endl;  
    }  
};  
  
int main() {  
    MyClass obj = {1, 2, 3, 4, 5}; // 使用 initializer_list 初始化对象  
    obj.print(); // 输出: 1 2 3 4 5  
  
    return 0;  
}

二、可变模版参数

1.可变模版参数简介

在C++98及这前的版本中类模版和函数模版中只能含固定数量的模版参数

C++11的新特性可变参数模板能够让您创建可以接受可变参数的函数模板和类模板

模板参数包是一个特殊的模板参数,它表示零个或多个模板参数。在函数模板或类模板的定义中,可以使用省略号(...)来声明一个模板参数包。

cpp 复制代码
// Args是一个模板参数包,args是一个函数形参参数包
// 声明一个参数包Args...args,这个参数包中可以包含0到任意个模板参数。
template<typename... Args>  
void print(Args... args) {  
    // 这里可以使用递归或循环来展开参数包  
    // 但更常见的是利用C++11的初始化列表展开(initializer list expansion)  
    // 在这里,我们只是简单地使用它们来演示  
    // 注意:实际打印需要更复杂的逻辑  
    // ...  
}  
  
// 使用  
print(1, 2.3, "Hello")



template<typename... Types>  
class Tuple {  
public:  
    // 类体,可能包含存储每种类型的成员变量  
    // 注意:这只是一个框架,实际实现需要更复杂的逻辑  
};  
  
// 使用  
Tuple<int, double, std::string> myTuple;

2.模板参数包展开的方式

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

1.递归

通过编写一个递归的模板函数,每次递归调用时从参数包中取出一个参数,直到参数包为空。这种方法需要定义一个递归终止函数,用于处理参数包为空的情况

cpp 复制代码
template<typename T, typename... Args>  
void print(T first, Args... args) {  
    std::cout << first << " ";  
    if constexpr (sizeof...(args) > 0) {  
        print(args...); // 递归调用  
    }  
    std::cout << std::endl;  
}  
 
template<typename T>  
void print(T last) {  
    std::cout << last << std::endl;  
}

2.逗号表达式

这种展开参数包的方式,不需要通过递归终止函数,是直接expand函数体中展开的, printarg不是一个递归终止函数,只是一个处理参数包中每一个参数的函数。

这种就地展开参数包的方式实现的关键是逗号表达式。我们知道逗号表达式会按顺序执行逗号前面的表达式。

cpp 复制代码
template<typename... Args>  
void print(Args... args) {  
    (void)std::initializer_list<int>{(std::cout << args << " ", 0)...};  
    std::cout << std::endl;  
}

3.示例 emplace_back

cpp 复制代码
template <class... Args>
void emplace_back (Args&&... args);

我们看到的emplace系列的接口,支持模板的可变参数,并且万能引用。那么相对insert和push_back,emplace系列接口的优势到底在哪里?
1. 直接在容器内构造对象
emplace系列接口允许直接在容器内部构造对象,而不需要先构造一个对象然后再将其插入容器中。这避免了额外的复制或移动构造函数调用,从而提高了性能。特别是当对象构造开销较大或者对象具有移动语义但不支持复制时,emplace的优势更加明显。
2. 模板可变参数支持
emplace接口支持模板的可变参数,这意味着它可以接受任意数量和类型的参数,并将这些参数直接传递给容器内部对象的构造函数。这使得emplace接口更加灵活,能够适应不同种类的对象构造需求。
3. 万能引用(Perfect Forwarding)
emplace接口通常利用C++11引入的完美转发(Perfect Forwarding)机制,通过std::forward等函数确保传递给构造函数的参数能够保持其原始的类型和值类别(左值或右值)。这意味着如果传递给emplace的是右值,那么对象将在容器内部以移动语义构造,进一步提高了效率。
4. 减少内存分配和拷贝次数
在某些情况下,使用emplace可以避免不必要的内存分配和对象拷贝。例如,在std::vector中,当向容器末尾添加元素且容量足够时,emplace_back可以直接在预留的空间内构造新对象,而无需重新分配内存或移动其他元素。

cpp 复制代码
#include <vector>  
#include <string>  
  
class MyClass {  
public:  
    MyClass(int x, std::string s) : x_(x), s_(std::move(s)) {}  
    // ... 其他成员函数 ...  
  
private:  
    int x_;  
    std::string s_;  
};  
  
int main() {  
    std::vector<MyClass> vec;  
  
    // 使用 push_back,需要先构造一个临时对象  
    vec.push_back(MyClass(1, "hello")); // 这里会调用 MyClass 的拷贝或移动构造函数  
  
    // 使用 emplace_back,直接在容器内构造对象  
    vec.emplace_back(2, "world"); // 这里会直接调用 MyClass 的构造函数  
  
    // ... 其他操作 ...  
}

在上面的示例中,emplace_back直接在vec的末尾构造了一个MyClass对象,而push_back则需要先构造一个临时的MyClass对象,然后再将其插入到vec中。这可能导致额外的拷贝或移动构造函数调用。

综上所述,emplace系列接口相比insert接口在性能、灵活性和减少内存分配/拷贝次数方面提供了显著的优势。因此,在需要向容器中添加新元素时,如果可能的话,应该优先考虑使用emplace接口。

相关推荐
小蜗牛慢慢爬行2 分钟前
有关异步场景的 10 大 Spring Boot 面试问题
java·开发语言·网络·spring boot·后端·spring·面试
Algorithm157612 分钟前
云原生相关的 Go 语言工程师技术路线(含博客网址导航)
开发语言·云原生·golang
用户00993831430117 分钟前
代码随想录算法训练营第十三天 | 二叉树part01
数据结构·算法
shinelord明21 分钟前
【再谈设计模式】享元模式~对象共享的优化妙手
开发语言·数据结构·算法·设计模式·软件工程
小手cool23 分钟前
List反转的方法
list
Monly2128 分钟前
Java(若依):修改Tomcat的版本
java·开发语言·tomcat
boligongzhu29 分钟前
DALSA工业相机SDK二次开发(图像采集及保存)C#版
开发语言·c#·dalsa
Eric.Lee202129 分钟前
moviepy将图片序列制作成视频并加载字幕 - python 实现
开发语言·python·音视频·moviepy·字幕视频合成·图像制作为视频
小俊俊的博客30 分钟前
海康RGBD相机使用C++和Opencv采集图像记录
c++·opencv·海康·rgbd相机
7yewh31 分钟前
嵌入式Linux QT+OpenCV基于人脸识别的考勤系统 项目
linux·开发语言·arm开发·驱动开发·qt·opencv·嵌入式linux