C++11新特性(可变参数模板、新的类功能、STL中的一些新变化)

可变参数模板

基本语法及原理

  • C++11支持可变参数模板,也就是说支持可变数量参数的函数模板和类模板,可变数目的参数被作为参数包,存在两种参数包:模板参数包,表示零或多个模板参数;函数参数包:表示零或多个函数参数

  • 格式:

    • 注意模板是...Args,参数类型是Args...
    • template<class ...Args> void Func(Args... args) {}
    • template<class ...Args> void Func(Args&... args) {}
    • template<class ...Args> void Func(Args&&... args) {}
  • 用省略号来指出一个模板参数或函数参数的表示一个包,在模板参数列表中,class...或typedef...指出接下来的参数表示零或多个类型列表;在函数参数列表中,类型名后面跟...指出接下来表示零或多个形参对象列表;函数参数包可以用左值引用或右值引用表示。每个参数实例化时遵循折叠规则。

  • 用 sizeof... 运算符计算参数包中参数的个数(不是sizeof,这是一个新的运算符)

    template <class ...Args>
    void Print(Args&&... args) //0-N个参数
    {
    cout << sizeof...(args) << endl;
    }

    int main()
    {
    double x = 2.2;
    Print(); //包里有0个参数
    Print(1); //包里有1个参数
    Print(1, string("xxx")); //包里有2个参数
    Print(1, string("xxx"), x); //包里有3个参数

    复制代码
      return 0;

    }

    //原理1:编译本质这里会结合引用折叠规则实例化出以下4个函数
    void Print();
    void Print(int&& arg1);
    void Print(int&& arg1, string&& arg2);
    void Print(double&& arg1, string&& arg2, double& arg3); //x是左值,所以double&

    //原理2:更本质去看有没有可变参数模板,我们是先出这样的多个函数模板才能支持
    // 这里的功能,有了可变参数模板,我们进一步被解放,他是类型泛化基础上
    // 叠加数量变化,让我们泛型编程更灵活
    void Print();

    template <class T1>
    void Print(T1&& arg1);

    template <class T1, class T2>
    void Print(T1&& arg1, T2&& arg2);

    template <class T1, class T2, class T3>
    void Print(T1&& arg1, T2&& arg2, T3&& arg3);

包扩展

  • 包扩展就是把它分解为构成的元素,对每个元素应用模式,获得扩展后的列表。

  • 我们通过在模式的右边放一个省略号(...)来触发扩展操作

  • 可变参数模板是编译时解析,而for(size_t i = 0; i < sizeof...(args); i++)是运行时获取和编译,所以不支持这样使用

  • 参数包展开的格式是 表达式(参数包)...... 必须写在整个表达式的末尾,用来触发=="对每个参数都执行一次表达式" ==的展开逻辑。例如:GetArg(args)... 的作用是:把参数包里的每个元素,分别传给 GetArg 函数,然后把所有 GetArg 的返回值,组成一个新的参数列表

    void ShowList()
    {
    //编译器时递归的终止条件,参数包是0个时,直接匹配这个函数
    cout << endl;
    }

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

    template<class ...Args>
    void Print(Args... args)
    {
    ShowList(args...);
    }

    int main()
    {
    Print();
    Print(1);
    Print(1, string("xxx"));
    Print(1, string("xxx"), 2.2);

    复制代码
      return 0;  

    }

    //Print(1, string("xxxxx"), 2.2);调用时
    //本质编译器将可变参数模板通过模式的包扩展,
    //编译器推导的以下三个重载函数函数
    void ShowList(double z)
    {
    cout << z << " ";
    ShowList();
    }
    void ShowList(string y, double z)
    {
    cout << y << " ";
    ShowList(z);
    }

    void ShowList(int x, string y, double z)
    {
    cout << x << " ";
    ShowList(y, z);
    }

    void Print(int x, string y, double z)
    {
    ShowList(x, y, z);
    }

![[包扩展底层细节.png]]

复制代码
template <class T>
const T& GetArg(const T& x)
{
    cout << x << " ";
    return x;
}

template <class ...Args>
void Arguments(Args... args)
{
}
template <class ...Args>
void Print(Args... args)
{
    Arguments(GetArg(args)...);
    //GetArg(args)...表示要展开,传入三个参数
    //实例化出Arguments(GetArg(x), GetArg(y), GetArg(z));
    //编译时展开为:Arguments(GetArg(1), GetArg(string("xxxxx")), GetArg(2.2));
}

int main()
{
    Print(1, string("xxxxx"), 2.2);
    return 0;
}

emplace接口

  • emplace系列的接口均为模板可变参数,功能上兼容push和insert系列。

  • emplace还支持直接插入构造T对象的参数,可直接在容器空间上构造T对象

  • 纯粹的左值插入和右值插入emplace_back和push_back是一样的;部分场景下,emplace可以直接构造,push和insert是 构造+移动构造 或 构造+拷贝构造

  • 综上,emplace更好用且强大,推荐用emplace系列替代push和insert

    int main()
    {
    listssp::string lt;
    //传左值,跟push_back一样,走拷贝构造
    ssp::string s1("111111"); //string(char* str) -- 构造
    lt.emplace_back(s1); //string(const string& s) -- 拷贝构造
    cout << "*********************************" << endl;

    复制代码
      //右值,跟push_back一样,走移动构造
      lt.emplace_back(move(s1));  //string(string&& s) -- 移动构造
      cout << "*********************************" << endl;
    
      //直接把构造string参数包往下传,直接用string参数包构造string
      //这里达到的效果是push_back做不到的
      lt.emplace_back("11111");   //string(char* str) -- 构造
      cout << "*********************************" << endl;
    
      list<pair<ssp::string, int>> lt1;
      //跟push_back一样
      //构造pair + 拷贝/移动构造pair到list的节点中data上
      pair<ssp::string, int> kv("苹果", 1); //string(char* str) -- 构造
      lt1.emplace_back(kv);   //string(const string& s) -- 拷贝构造
      cout << "*********************************" << endl;
    
      //跟push_back一样
      lt1.emplace_back(move(kv)); //string(string&& s) -- 移动构造
      cout << "*********************************" << endl;
    
      //直接把构造pair参数包往下传,直接⽤pair参数包构造pair
      //这里达到的效果是push_back做不到的
      lt1.emplace_back("苹果", 1);  //string(char* str) -- 构造
      cout << "*********************************" << endl;
    
      return 0;

    }

  • 万能引用和完美转发使用

    //构造函数
    template <class ...Args>
    ListNode(Args&&... args)
    : _next(nullptr)
    , _prev(nullptr)
    , _data(std::forward(args)...)
    {}

    //emplace_back实现
    template void emplace_back(Args&&... args)
    {
    insert(end(), std::forward(args)...);
    }

...的所有位置

声明参数包

模板参数列表中:声明类型参数包
  • 格式:template <class... Args>template <typename... Args>

    // ... 跟在Args后面,声明Args是"类型参数包"(装0~N个类型)
    template <class... Args>
    void Print(Args&&... args); // 这里的Args就是上面声明的类型包

函数参数列表中:声明值参数包
  • 格式:格式:void Func(T... args)(T 是类型,args 是值参数包)

    // 先声明类型包Ts,再用Ts...声明值参数包args
    template <typename... Ts>
    void Func(Ts... args)
    {
    // args是值参数包,装0~N个值
    }

展开参数包

函数调用中展开
  • 格式:函数名(表达式(参数包)...)

  • 关键:GetArg(args) 是 "包含参数包的表达式",... 跟在这个表达式后面,才会对每个参数执行一次 GetArg

    template <class... Args>
    void Print(Args&&... args)
    {
    // 核心:GetArg(args)... 是"表达式(参数包)...",...在末尾触发展开
    Arguments(GetArg(args)...);
    // 展开逻辑(编译时):
    // Print(1, "xxx", 2.2) → Arguments(GetArg(1), GetArg("xxx"), GetArg(2.2))
    }

初始化列表 / 数组中展开
  • 格式:{表达式(参数包)...}

    template <typename... Ts>
    void PrintSize(Ts... args)
    {
    // 展开参数包,计算每个参数的大小,存入数组
    int sizes[] = {sizeof(args)...};
    // 展开逻辑:
    // PrintSize(1, "xxx", 2.2) → int sizes[] = {sizeof(1), sizeof("xxx"), sizeof(2.2)};
    for (auto s : sizes)
    {
    cout << s << " ";
    // 输出:4 4 8(不同编译器可能有差异)
    }
    }

递归展开
  • 格式:函数名(表达式(参数包)...)(递归调用自身,逐步拆包)

    // 递归终止函数(参数包为空时调用)
    void RecursivePrint() {
    cout << endl;
    }

    // 递归展开函数(拆出第一个参数,剩下的继续递归)
    template <typename T, typename... Ts>
    void RecursivePrint(T first, Ts... rest) {
    cout << first << " ";
    RecursivePrint(rest...); // ...跟在rest后面,展开剩余参数包
    }

    // 调用:RecursivePrint(1, "xxx", 2.2);
    // 执行流程:
    // 1. RecursivePrint(1, "xxx", 2.2) → 输出1,调用RecursivePrint("xxx", 2.2)
    // 2. RecursivePrint("xxx", 2.2) → 输出xxx,调用RecursivePrint(2.2)
    // 3. RecursivePrint(2.2) → 输出2.2,调用RecursivePrint()
    // 4. RecursivePrint() → 输出换行,结束

万能引用中&& + ...

复制代码
template <class... Args>
void Print(Args&&... args) { // Args&&... 是"万能引用的参数包"
    // Args&& 是万能引用,... 是参数包展开,两者结合表示"0~N个万能引用参数"
}

新的类功能

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

  • 在之前的C++类中,有6个默认成员函数:构造函数/析构函数/拷贝构造函数/拷贝赋值重载/取地址重载/const取地址重载

  • C++11新增了两个默认成员函数,移动构造函数和移动赋值运算符重载

  • 如果没有自己实现移动构造函数,且没有实现析构函数、拷贝构造、拷贝赋值重载中的任意一个,那么编译器会自动生成一个默认移动构造函数。

  • 默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝;自定义类型成员,则需要看这个成员是否实现了移动构造,如果实现了就是调用移动构造,没有实现就调用拷贝构造

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

    int main()
    {
    Person s1; //string(char* str) -- 构造
    Person s2 = s1; //string(const string& s) -- 拷贝构造
    Person s3 = std::move(s1); //string(string&& s) -- 移动构造
    Person s4; //string(char* str) -- 构造
    s4 = std::move(s2); //string& operator=(string&& s) -- 移动赋值

    复制代码
      return 0;

    }

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

  • 成员变量声明时给的缺省值是给初始化列表用的,如果没有显示在初始化列表初始化,就会在初始化列表用这个值初始化
  • 使用优先级:显示传参 > 有初始化列表的参数列表中的缺省值 > 无初始化列表的声明时给的缺省值
  • 没有初始化列表就用声明时给的缺省值,有的话就用显示传参/参数列表的缺省值

default和delete

  • default强制生成某个默认的函数,比如我们提供了拷贝构造,就不会生成移动构造了,那么我们可以使用default关键字显示指定移动构造生成

  • 想要限制某些默认函数的生成,只需在该函数声明加上 =delete 即可,该语法指示编译器不生产对应函数的默认版本,称 =delete修饰的函数为删除函数

    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;   //强制生成移动构造
    
      Person(const Person& p) = delete;   //限制生成拷贝构造(禁止调用)

    private:
    ssp::string _name;
    int _age;
    };

    int main()
    {
    Person s1;
    //Person s2 = s1; //不能使用拷贝构造
    Person s3 = std::move(s1);
    return 0;
    }

final和override

  • 不想让派生类重写某个虚函数,可以用final修饰

    class Car {
    public:
    virtual void Dirve() final
    {}
    };

    class Benz :public Car
    {
    public:
    // error C3248: "Car::Dirve": 声明为 "final" 的函数不能由 "Benz::Dirve" 重写
    virtual void Dirve()
    {
    cout << "Benz舒适" << endl; }
    };
    }

  • override可以帮助用户检测是否正确重写了虚函数(若是重写正确无影响,不正确会直接报错)

    class Car {
    public:
    virtual void Dirve()
    {}
    };

    class Benz :public Car
    {
    public:
    virtual void Dirve() override
    {
    cout << "Benz舒适" << endl;
    }
    };

STL中有哪些新变化

  • STL中新添了一些新容器,unordered_map,unordered_set,array,forward_list
  • STL容器中增加了新接口,最重要的就是右值引用和移动语义相关的push/insert/emplace系列接口,移动构造和移动赋值,还有initializer_list版本的构造
  • 容器的范围for遍历
相关推荐
lxl13072 小时前
学习C++(6)日期类的实现+取地址运算符重载
开发语言·c++·学习
fqbqrr2 小时前
2602c++,剪切板格式
c++
我材不敲代码2 小时前
Python爬虫介绍——简单了解一下爬虫
开发语言·爬虫·python
strings_lei2 小时前
AI 学习笔记 - AWS 相关服务
笔记·学习
Howrun7772 小时前
可调用对象
开发语言·c++
BlackWolfSky2 小时前
鸿蒙中级课程笔记11—元服务开发
笔记·华为·harmonyos
2 小时前
java关于引用
java·开发语言
小小码农Come on2 小时前
QT布局介绍
开发语言·qt
晚风吹长发2 小时前
初步了解Linux中的线程概率及线程控制
linux·运维·服务器·开发语言·c++·centos·线程