【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接口。

相关推荐
时光の尘12 分钟前
C语言菜鸟入门·关键字·float以及double的用法
运维·服务器·c语言·开发语言·stm32·单片机·c
我们的五年16 分钟前
【Linux课程学习】:进程描述---PCB(Process Control Block)
linux·运维·c++
以后不吃煲仔饭26 分钟前
Java基础夯实——2.7 线程上下文切换
java·开发语言
进阶的架构师26 分钟前
2024年Java面试题及答案整理(1000+面试题附答案解析)
java·开发语言
前端拾光者30 分钟前
利用D3.js实现数据可视化的简单示例
开发语言·javascript·信息可视化
程序猿阿伟32 分钟前
《C++ 实现区块链:区块时间戳的存储与验证机制解析》
开发语言·c++·区块链
傻啦嘿哟1 小时前
如何使用 Python 开发一个简单的文本数据转换为 Excel 工具
开发语言·python·excel
大数据编程之光1 小时前
Flink Standalone集群模式安装部署全攻略
java·大数据·开发语言·面试·flink
初九之潜龙勿用1 小时前
C#校验画布签名图片是否为空白
开发语言·ui·c#·.net
Lenyiin1 小时前
02.06、回文链表
数据结构·leetcode·链表