C++11相关新特性(列表初始化、右值引用、可变参数模版)

目录

C++11相关新特性

列表初始化

初始化简单变量

初始化容器

decltype关键字

C++ 11新增的容器

左值引用和右值引用

左值与右值

左值引用与右值引用

左值引用和右值引用的相互转化

右值引用的使用

拷贝构造函数与移动构造函数

赋值重载函数与移动赋值重载函数

元素插入相关函数

万能引用与完美转发

万能引用

完美转发

C++ 11新增的两个默认成员函数

生成默认成员函数=default

不生成默认成员函数=delete

C++ 11中的可变参数模版

可变参数模版介绍

参数包展开

可变参数模版的应用emplace系列函数

emplace系列函数和push系列函数的选择


C++11相关新特性

列表初始化

初始化简单变量

在C语言和C++98中,对一中类型的变量进行初始化时,主要使用赋值符号与初始化值,如果是一个数组或者结构体等具有多个成员的变量初始化时,可以使用{}进行初始化

struct point
{
    int x;
    int y;
};

int main()
{
    // 初始化一个内置类型变量
    int a = 0;
    // 初始化一个数组
    int data[] = { 2,3,5,4 };
    // 初始化结构
    point p = { 2, 1 };

    return 0;
}

上面的代码中需要注意对于一个数组和结构来说,为了兼容C语言,默认是使用{}进行初始化,此时不算作列表初始化

在C++11中,可以使用{}对所有类型的变量进行初始化,并且可以省略赋值符号

struct point
{
    int x;
    int y;
};

int main()
{
    // 列表初始化并省略赋值符号
    int a1{ 0 };
    int data1[]{ 1,2,3,4 };
    point p1{ 2, 1 };

    return 0;
}

需要注意,如果使用了列表初始化,则不可以出现部分用于初始化的值赋值给变量后出现数据丢失的情况,例如double的值赋值给int的变量

// double转int
double num1 = 3.14;
int a3 = { num1 };

// long long 转 int
long long num = 648797LL;
int a2 = {num};

// long 转 int
// 正常编译
long num2 = 648797L;
int a4 = { num2 };

报错信息:
conversion from '__int64' to 'int' requires a narrowing conversion     
conversion from 'double' to 'int' requires a narrowing conversion

初始化容器

有了列表初始化后,容器的初始化可以变得更加简单,对比下面的初始化方式

int main()
{
    // 初始化vector
    // C++ 98的初始化
    int data[] = { 0,1,2,3,4,5,6 };
    vector<int> v;
    for (auto num : data)
    {
        v.push_back(num);
    }

    // C++ 11的初始化
    vector<int> v{ 0,1,2,3,4,5,6 };

    return 0;
}

对于map来说,对比下面的初始化方式

int main()
{
    // 初始化map
    // C++ 98的初始化
    map<int, int> m;
    m.insert({ 1, 1 });
    m.insert({ 2, 2 });
    m.insert({ 3, 3 });
    m.insert({ 4, 4 });
    m.insert({ 5, 5 });

    // C++ 11的初始化
    map<int, int> m1{{ 1,1 }, { 2,2 }, { 3,3 }};

    return 0;
}

在上面的代码中,对于C++ 98的初始化来说,通过多参数构造的隐式类型转换作为参数传递给insert()函数,而C++ 11中,结合了类型转换已经列表初始化对map进行初始化

decltype关键字

前面声明变量时使用auto关键字,根据赋值符号右侧类型推导变量的类型,但是如果没有赋值,auto此时不可以进行推导;为了知道某一个变量的类型,可以使用typeid(变量).name()进行获取,直接打印即可查看指定变量的类型

但是上面两种方式都无法做到根据已有变量/常量的类型创建新的变量,为了解决这个问题,在C++ 11中新增了decltype关键字

int main()
{
    int x = 0;
    // 获取普通变量类型创建变量
    decltype(x) x1 = 2;
    // 获取表达式的值类型创建变量
    decltype(1 + 2) x2 = 3;
    decltype(1 + 2.1) x3 = 3;

    cout << typeid(x1).name() << endl;
    cout << typeid(x2).name() << endl;
    cout << typeid(x3).name() << endl;

    return 0;
}

输出结果:
int
int
double

C++ 11新增的容器

下面红色标记的容器均为C++11新增的容器

<array>:封装的是C语言静态数组,本质还是普通数组,只是为了便于控制数组越界等问题,因为一般的数组越界读写在编译阶段是不容易检测出来的

<forward_list>:单链表,这个单链表没有尾插和尾删,因为开销大

<unordered_map><unordered_set>:封装的是哈希表

左值引用和右值引用

左值与右值

左值代表赋值符号左侧的值,可以直接取地址,一般为变量,并且一般情况下可以修改(被const修饰的左值不可以修改)

右值代表赋值符号右侧的值,不可以直接取地址,一般为内置类型常量、函数返回值和表达式的值

右值不可以出现在赋值符号的左侧,否则会报错为不可修改的左值,但是左值可以出现在右侧,此时是将左值中的值赋值给赋值符号左侧的新左值,例如int b = 0; a = b;

右值可以分为两种

  1. 纯右值:一般为内置类型常量
  2. 将亡值:一般为函数返回值中的临时对象、匿名对象等即将被销毁的值

左值引用与右值引用

左值引用:即为对左值的引用,在类型后加一个&即可代表左值引用类型,例如int num = 0; int& ref = num;ref为左值num的别名,表达式int& ref = num;中的refnum均为左值

右值引用:即为对右值的别名,在类型后加两个&即可代表右值引用类型,例如int&& ref = 1;,表达式中的ref为对右值引用的左值,1为右值

一般情况下,左值引用的对象不可以是常量,因为临时变量具有常性,为了解决这个问题,可以使用 const修饰左值引用,例如 const int& num = 1;

左值引用和右值引用的相互转化

左值引用可以通过强制转换转化为右值引用,也可以通过move()函数进行变换,例如下面的代码

int main()
{
    int num = 0;
    // 左值引用
    int& r1 = num;

    // 将左值引用强制转换为右值引用
    int&& r2 = (int&&)r1;
    int&& r3 = move(r1);
    return 0;
}

右值引用可以通过强制转换转化成左值引用,例如下面的代码

int main()
{
    int num = 0;
    // 左值引用
    int& r1 = num;

    // 将左值引用强制转换为右值引用
    int&& r2 = (int&&)r1;

    // 将右值引用强制转化为左值引用
    int& r3 = (int&)r2;

    return 0;
}

之所以可以通过强制转换实现左值引用和右值引用之间的相互转换是因为左值引用和右值引用在底层实际上都是一样的,只是语法层面对二者进行了更严格的定义,只要基本类型相同就可以通过强制转换进行改变,参考下图的汇编代码:

右值引用的使用

以模拟实现的string为例

namespace simulate_string
{
    class string
    {
    public:
        typedef char* iterator;
        iterator begin()
        {
            return _str;
        }
        iterator end()
        {
            return _str + _size;
        }

        typedef const char* const_iterator;
        const_iterator begin() const
        {
            return _str;
        }

        const_iterator end() const
        {
            return _str + _size;
        }

        string(const char* str = "")
            :_size(strlen(str))
            , _capacity(_size)
        {
            cout << "string(char* str)" << endl;
            _str = new char[_capacity + 1];
            strcpy(_str, str);
        }

        // s1.swap(s2)
        void swap(string& s)
        {
            ::swap(_str, s._str);
            ::swap(_size, s._size);
            ::swap(_capacity, s._capacity);
        }

        // 拷贝构造
        string(const string& s)
            :_str(nullptr)
        {
            cout << "string(const string& s) -- 深拷贝" << endl;

            reserve(s._capacity);
            for (auto ch : s)
            {
                push_back(ch);
            }
        }

        // 赋值重载
        string& operator=(const string& s)
        {
            cout << "string& operator=(const string& s) -- 深拷贝" << endl;
            if (this != &s)
            {
                _str[0] = '\0';
                _size = 0;

                reserve(s._capacity);
                for (auto ch : s)
                {
                    push_back(ch);
                }
            }

            return *this;
        }

        ~string()
        {
            delete[] _str;
            _str = nullptr;
        }

        void reserve(size_t n)
        {
            if (n > _capacity)
            {
                char* tmp = new char[n + 1];
                if (_str)
                {
                    strcpy(tmp, _str);
                    delete[] _str;
                }
                _str = tmp;
                _capacity = n;
            }
        }

        void push_back(char ch)
        {
            if (_size >= _capacity)
            {
                size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;
                reserve(newcapacity);
            }

            _str[_size] = ch;
            ++_size;
            _str[_size] = '\0';
        }

        //string operator+=(char ch)
        string& operator+=(char ch)
        {
            push_back(ch);
            return *this;
        }

    private:
        char* _str = nullptr;
        size_t _size = 0;
        size_t _capacity = 0; // 不包含最后做标识的\0
    };

    simulate_string::string to_string(int value)
    {
        bool flag = true;
        if (value < 0)
        {
            flag = false;
            value = 0 - value;
        }
        simulate_string::string str;

        while (value > 0)
        {
            int x = value % 10;
            value /= 10;
            str += ('0' + x);
        }

        if (flag == false)
        {
            str += '-';
        }

        std::reverse(str.begin(), str.end());

        return str;
    }
}
拷贝构造函数与移动构造函数

在前面对于需要深拷贝的类来说,需要自己写类的拷贝构造函数,在没有移动构造函数时,只要是涉及到对象的拷贝都会调用拷贝构造函数,包括但不限于返回临时对象,例如下面的代码:

simulate_string::string to_string(int value)
{
    bool flag = true;
    if (value < 0)
    {
        flag = false;
        value = 0 - value;
    }
    simulate_string::string str;

    while (value > 0)
    {
        int x = value % 10;
        value /= 10;
        str += ('0' + x);
    }

    if (flag == false)
    {
        str += '-';
    }

    std::reverse(str.begin(), str.end());

    return str;
}

上面的代码中模拟实现了to_string函数,函数返回一个simulate_string::string类的对象str,调用该函数:

int main()
{
    simulate_string::string s = simulate_string::to_string(123);

    return 0;
}

在编译器没有优化并且只有一个拷贝构造函数时,当执行main函数中的第一条语句时,执行过程如下:

当编译器检测到当前写法simulate_string::string s = simulate_string::to_string(123);时,会进行优化,所以执行过程优化为直接调用拷贝构造函数构造对象s,如下图所示:

但是,进入拷贝构造函数中拷贝str的内容会存在一定的空间和时间消耗,所以为了解决这个问题,可以采用移动拷贝构造,移动拷贝构造本质就是利用了右值引用。str返回时,在未被编译器优化的情况下会生成一个临时对象,这个临时对象会进行一次拷贝,但因为临时对象是属于将亡值,所以使用右值引用的移动构造会更加方便且高效,只需要将临时对象中的值和现有对象中的值进行交换即可,移动构造如下:

// 移动构造
string(string&& s)
{
    swap(s);
}

有了移动构造函数以后,对于需要使用返回的临时对象进行拷贝构造时就会直接走移动构造,从而减少原来拷贝构造的消耗

需要注意的是,为了确保可以交换成功形式参数不可以使用 const修饰

在C++ 11中,构造函数也包括了移动构造,例如string类中的移动构造:

赋值重载函数与移动赋值重载函数

上面的移动构造只解决了在拷贝临时对象时会调用拷贝构造函数产生的消耗问题,如果main函数的代码修改为如下:

int main()
{
    simulate_string::string s;
    s = simulate_string::to_string(123);

    return 0;
}

此时移动构造就无法解决问题,因为是赋值符号重载函数与临时对象之间的关系,同样,当编译器未进行优化时会进行下面的过程:

当编译器优化后,会直接调用一次赋值重载函数,用str对象为s对象赋值

但是尽管编译器进行了优化,赋值重载函数因为是将str中的内容深拷贝给s对象,所以依旧会产生开销,当对象很大时,开销也会变得很大。因为str是将亡值,所以可以采用右值引用的方式,重载一个新的赋值重载函数如下,同样只需要交换一下将亡值和当前对象中的内容即可:

// 移动赋值
string& operator=(string&& s)
{
    cout << "string& operator=(string&& s) -- 移动赋值" << endl;

    swap(s);
    return *this;
}

在C++ 11中,赋值重载函数也包括了移动赋值重载函数,例如string类中的移动赋值重载函数:

元素插入相关函数

以模拟实现的list类为例

#pragma once

#include <iostream>
#include <assert.h>
using namespace std;

namespace simulate_list
{
    template<class T>
    struct ListNode
    {
        ListNode<T>* _next;
        ListNode<T>* _prev;

        T _data;

        ListNode(const T& data = T())
            :_next(nullptr)
            , _prev(nullptr)
            , _data(data)
        {}
    };

    template<class T, class Ref, class Ptr>
    struct ListIterator
    {
        typedef ListNode<T> Node;
        typedef ListIterator<T, Ref, Ptr> Self;
        Node* _node;

        ListIterator(Node* node)
            :_node(node)
        {}

        // ++it;
        Self& operator++()
        {
            _node = _node->_next;
            return *this;
        }

        Self& operator--()
        {
            _node = _node->_prev;
            return *this;
        }

        Self operator++(int)
        {
            Self tmp(*this);
            _node = _node->_next;

            return tmp;
        }

        Self& operator--(int)
        {
            Self tmp(*this);
            _node = _node->_prev;

            return tmp;
        }

        Ref operator*()
        {
            return _node->_data;
        }

        Ptr operator->()
        {
            return &_node->_data;
        }

        bool operator!=(const Self& it)
        {
            return _node != it._node;
        }

        bool operator==(const Self& it)
        {
            return _node == it._node;
        }
    };

    template<class T>
    class list
    {
        typedef ListNode<T> Node;
    public:

        typedef ListIterator<T, T&, T*> iterator;
        typedef ListIterator<T, const T&, const T*> const_iterator;

        iterator begin()
        {
            return iterator(_head->_next);
        }

        const_iterator begin() const
        {
            return const_iterator(_head->_next);
        }

        iterator end()
        {
            return iterator(_head);
        }

        const_iterator end() const
        {
            return const_iterator(_head);
        }

        void empty_init()
        {
            _head = new Node();
            _head->_next = _head;
            _head->_prev = _head;
        }

        list()
        {
            empty_init();
        }

        list(initializer_list<T> il)
        {
            empty_init();

            for (const auto& e : il)
            {
                push_back(e);
            }
        }

        // lt2(lt1)
        list(const list<T>& lt)
        {
            empty_init();

            for (const auto& e : lt)
            {
                push_back(e);
            }
        }

        // lt1 = lt3
        list<T>& operator=(list<T> lt)
        {
            swap(_head, lt._head);

            return *this;
        }

        ~list()
        {
            clear();
            delete _head;
            _head = nullptr;
        }

        void clear()
        {
            auto it = begin();
            while (it != end())
            {
                it = erase(it);
            }
        }

        void push_back(const T& x)
        {
            insert(end(), x);
        }

        void pop_back()
        {
            erase(--end());
        }

        void push_front(const T& x)
        {
            insert(begin(), x);
        }

        void pop_front()
        {
            erase(begin());
        }

        // 没有iterator失效
        iterator insert(iterator pos, const T& x)
        {
            Node* cur = pos._node;
            Node* newnode = new Node(x);
            Node* prev = cur->_prev;

            // prev  newnode  cur
            prev->_next = newnode;
            newnode->_prev = prev;
            newnode->_next = cur;
            cur->_prev = newnode;

            return iterator(newnode);
        }

        // erase 后 pos失效了,pos指向节点被释放了
        iterator erase(iterator pos)
        {
            assert(pos != end());

            Node* cur = pos._node;
            Node* prev = cur->_prev;
            Node* next = cur->_next;

            prev->_next = next;
            next->_prev = prev;

            delete cur;

            return iterator(next);
        }

    private:
        Node* _head;
    };
}

当在main函数中创建对象后进行尾插:

int main()
{
    simulate_list::list<simulate_string::string> ls;

    ls.push_back("11111");
    
    return 0;
}

因为"1111"属于常量字符串,属于右值,在push_back函数中会调用Node节点的构造函数初始化_data,而因为_datasimulate_string类型的,所以会调用对应的构造函数将其转化为simulate_string类型,接着再链接,但是整个过程会涉及到simulate_string类的拷贝构造函数,并且因为_data是左值,所以在进入simulate_string类后也是左值,就不会调用前面的移动拷贝构造函数,这个现象也称为右值退化为左值,为了解决这个问题,首先需要修改push_back函数,将"11111"识别为右值,所以需要使用右值引用作为push_back函数的形式参数,所以push_back函数需要重载一份为如下形式:

void push_back(T&& x)
{
    insert(end(), x);
}

接着,因为push_back底层调用的还是insert函数,所以insert函数的形式参数也需要重载一份为如下形式:

iterator insert(iterator pos, T&& x)
{
    Node* cur = pos._node;
    Node* newnode = new Node(x);
    Node* prev = cur->_prev;

    // prev  newnode  cur
    prev->_next = newnode;
    newnode->_prev = prev;
    newnode->_next = cur;
    cur->_prev = newnode;

    return iterator(newnode);
}

但是,仅仅修改这两个函数的形式参数并不能解决问题,首先看push_back函数,因为底层调用的是insert函数,所以需要将push_back的右值引用x接收到的值继续传递给insert函数,此时需要注意,右值引用本身还是左值,所以传递给insert函数参数的x依旧还是左值,此时尽管写了重载右值引用的insert函数,依旧会调用左值引用的insert函数,所以需要对push_back函数进行进一步的修改,如下形式:

void push_back(T&& x)
{
    insert(end(), move(x));// 将左值的右值引用转化为右值的右值引用
}

接着到insert函数,在push_back函数修改为上述形式后,此时调用的insert函数即为重载右值引用的版本,接下来创建节点,此时依旧是同样的问题,x是右值引用,但是本身是左值,所以传递给Node节点的构造函数时也依旧是左值,所以同样需要将其转化为右值,如下形式:

iterator insert(iterator pos, T&& x)
{
    Node* cur = pos._node;
    Node* newnode = new Node(x);// 将左值的右值引用转化为右值的右值引用
    // ...
}

接着是Node节点的构造函数,当前情况下只有一个左值引用版本的构造函数,所以需要重载一个右值引用版本,如下形式:

ListNode(T&& data)
    :_next(nullptr)
    , _prev(nullptr)
    , _data(data)
{}

但是上面的代码依旧是同样的问题,形式参数data是右值引用,但本身是左值,所以需要将data转化为右值,修改为:

ListNode(T&& data)
    :_next(nullptr)
    , _prev(nullptr)
    , _data(move(data))
{}

此时再调用simulate_string类的构造函数时,就会只调用移动拷贝构造函数

在C++ 11中,元素插入相关的函数也包括了右值引用的版本,例如list类中的右值引用版本的push_back函数:

万能引用与完美转发

万能引用

万能引用可以将在函数模版中使用,形式如下:

// 万能引用
template<typename T>
void func(T&& t)
{

}

在上面的代码中,T&&是一个万能引用,当传递左值时,T被推导为左值引用,当传递右值时,T被推导为右值引用,但是不论T被推导为左值引用还是右值引用,形参t本身依旧是左值,所以当需要再向下传递需要使用到右值时,依旧需要将t进行转化,例如下面的例子:

// 查看引用类型
void Func(int& x) 
{ 
    cout << "左值引用" << endl; 
}

void Func(int&& x) 
{ 
    cout << "右值引用" << endl; 
}

// 万能引用
template<typename T>
void func(T&& t)
{
    // 万能引用的类型推导
    // T&&是一个万能引用,当传递左值时,T被推导为左值引用
    // 当传递右值时,T被推导为右值引用
    Func(t);
}

int main()
{
    // 左值传递给func函数
    int a = 10;
    func(a);
    // 右值传递给func函数
    func(20);
    // 将左值move为右值
    func(move(a));

    return 0;
}

输出结果:
左值引用
左值引用
左值引用

此时不论t是左值引用还是右值引用,t本身都是左值,所以传递给Func函数只会走打印“左值引用”的部分,如果使用move对形参t进行转化,那么只会走打印“右值引用”的Func函数,为了解决这个问题,可以使用完美转发

完美转发

完美转发可以将变量原有的类型传递给下一层,如果本身是左值引用,则完美转发后就是左值引用,如果本身是右值引用,则完美转发后就是右值引用,所以上面的代码可以写成:

// 查看引用类型
void Func(int& x) 
{ 
    cout << "左值引用" << endl; 
}

void Func(int&& x) 
{ 
    cout << "右值引用" << endl; 
}

// 万能引用
template<typename T>
void func(T&& t)
{
    // 使用完美转发
    Func(forward<T>(t));
}

int main()
{
    // 左值传递给func函数
    int a = 10;
    func(a);
    // 右值传递给func函数
    func(20);
    // 将左值move为右值
    func(move(a));

    return 0;
}

输出结果:
左值引用
右值引用
右值引用

有了完美转发后,就可以对上面list模拟实现中的插入函数进行修改,前面遇到的问题就是本身是右值,给了右值引用再向下传递时发生右值引用退化为左值引用,所以可以使用完美转发使其按照右值引用的方式传递,以push_back为例

void push_back(T&& x)
{
    insert(end(), forward<T>(x));// 将左值的右值引用转化为右值的右值引用
}

C++ 11新增的两个默认成员函数

在有了移动拷贝构造函数和移动赋值重载函数后,类的默认成员函数从原有的6个变为了现在的8个,对于新增的两个默认成员函数来说,默认的生成规则如下:

  • 移动拷贝构造函数:当类中没有显式写移动拷贝构造函数并且类中也没有显式写拷贝构造函数、析构函数和赋值重载函数时,编译器会默认生成移动拷贝构造函数,该函数的行为是,当类中的成员是内置类型时,将会进行值拷贝,当类中的成员是自定义类型时,会调用自定义类型的移动拷贝构造函数,如果自定义类型也没有移动拷贝构造函数,则调用拷贝构造函数
  • 移动赋值重载函数:当类中没有显式写移动赋值重载函数并且类中也没有显式写拷贝构造函数、析构函数和赋值重载函数时,编译器会默认生成移动赋值重载函数,该函数的行为是,当类中的成员是内置类型时,将会进行值拷贝,当类中的成员是自定义类型时,会调用自定义类型的移动赋值重载函数,如果自定义类型也没有移动赋值重载函数,则调用赋值重载函数
之所以需要满足两个条件(1. 没有显式写移动拷贝构造函数 2. 没有显式写拷贝构造函数、析构函数和赋值重载函数)可以考虑移动赋值重载函数和移动拷贝构造函数的使用场景:当类中的成员都是内置类型时,没有大型资源释放行为,所以不需要显示写析构函数,此时对于拷贝构造函数和赋值重载函数来说,直接复制原有对象的内容开销也不大,而对于移动拷贝构造函数和移动赋值重载函数来说,目的就是解决拷贝构造函数和赋值重载函数在部分场景下的时间和空间消耗,所以没有拷贝构造函数、析构函数和赋值重载函数代表没有大型的资源释放行为,自然也就可以不会产生大量的时间和空间消耗,所以也就不需要写移动拷贝构造函数和移动赋值重载函数

生成默认成员函数=default

如果一定要生成默认的移动拷贝构造和移动赋值重载函数时,可以使用=default关键字,例如如果需要默认生成string类的拷贝构造函数,可以写成:

string(const string& s)=default;
需要注意的是,如果想让编译器默认生成移动拷贝构造函数和移动赋值重载函数时,一定要有拷贝构造函数、析构函数和赋值重载函数的出现,哪怕是使用 =default让移动拷贝构造函数和移动赋值重载函数默认生成,不可以缺少三个默认成员函数的一个,否则无法通过编译

不生成默认成员函数=delete

如果不想编译器默认生成某一个默认成员函数时,可以使用=delete关键字,例如如果不像默认生成string类的拷贝构造函数,可以写成:

string(const string& s)=delete;

在C++ 98中,如果不想一个对象可以通过调用拷贝构造函数和赋值重载函数进行构造可以将对应的拷贝构造函数和赋值重载函数修饰为private,而在有了C++ 11的=delete关键字后,就可以对这两个函数修饰为=delete,而无需在放入private

C++ 11中的可变参数模版

可变参数模版介绍

在C++ 98中,如果一个函数是一个模版函数,那么该函数可以传递的参数个数就由模版参数个数决定,如果传递参数多于或少于(此处不考虑含有缺省参数的情况)规定的模版个数,则编译器无法生成对应的函数

在C++ 11中,为了解决上面的问题提出了可变参数的函数模版,基本格式如下:

// 可变参数模版
template<class... Args>
void func(Args... args)
{

}

在上面的代码中使用...代表可变参数,...Args代表模版参数包,... args代表形式参数包,参数包中可以有[0, N](N >= 0且N为整数)个模版参数,此时编译器会根据传递的参数个数生成对应的函数。

如果想要获取函数参数的数量时,可以使用sizeof运算符计算形式参数包,代码如下:

// 可变参数模版
template<class... Args>
void func(Args... args)
{
    cout << sizeof...(args) << endl;
}
sizeof计算属于编译时就可以计算的,所以可以直接使用,需要注意省略号的所在位置

如果想在函数func中查看传递的参数时则不可以使用遍历等运行时的逻辑进行打印,例如使用for循环

template<class... Args>
void func(Args... args)
{
    //cout << sizeof...(args) << endl;
    for (size_t i = 0; i < sizeof...(args); i++)
    {
        cout << args[i] << endl;
    }
}

报错信息:
'args': parameter pack must be expanded in this context

参数包展开

为了能够展示参数,下面采用两种方法进行显示,以下面的代码为例:

int main()
{
    // 可变参数模版
    func();
    func(1, 2, 3);
    func(1, "hello", 3.14);

    return 0;
}
  1. 编译时递归展开参数包

编译时递归和运行时递归的最大区别就是不可以使用if语句进行递归结束条件的判断

编译时递归展开参数包的思路是:创建一个有可变参数模版的函数func,该模版参数含有两部分,第一个部分是万能引用的单一参数,该部分用于接收每一个参数值,第二个部分是可变参数模版,在函数体内,先打印第一个参数的内容,再调用func函数,传递剩余的参数,用于递归调用,再外侧写一个无参的func函数,该函数作为编译时递归的终止条件

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

template<class T, class... Args>
void func(T&& x, Args... args)
{
    // 打印当前的x
    cout << x << " ";
    // 递归调用打印剩余的参数
    func(forward<Args>(args)...);
}

对于上面的测试函数,结果如下:

1 2 3
1 hello 3.14

以第二个测试为例func(1, 2, 3);,上面的代码可以理解为:

//4. 第四步
void func()
{
    cout << endl;
}
//3. 第三步
void func(int&& z)
{
    cout << z << " ";
    func();
}
//2. 第二步
void func(int&& y, int&& z)
{
    cout << y << " ";
    func(forward<int>(z));
}
//1. 第一步
void func(int&& x, int&& y, int&& z)
{
    // 打印当前的x
    cout << x << " ";
    // 递归调用打印剩余的参数
    func(forward<int>(y), forward<int>(z));
}
  1. 利用数组根据个数初始化数组大小的机制展开参数包

该方式的原理是:当一个数组在初始化时,如果不指定数组的大小,编译器会根据数组的元素个数推导出数组的大小,所以可以写为:

template <class... Args>
void func(Args&&... args)
{
    int arr[] = { ((cout << forward<Args>(args) << " ", 0))... };
    cout << endl;
}
这个方法需要注意,对于没有实参的函数来说会编译报错,例如 func()函数,并且不可以去掉逗号表达式,因为 cout的返回值是 ostream类型,该类型不支持赋值运算符重载函数

上面的方法可以理解为:

// 上面的展开可以理解为
void func(int&& x, int&& y, int&& z)
{
    int arr[] = { ((cout << forward<int>(x) << " ", 0)), ((cout << forward<int>(y) << " ", 0)), ((cout << forward<int>(z) << " ", 0)) };
    cout << endl;
}

因为数组在开辟大小是会计算元素的个数,逗号表达式的左右两侧都会进行运算,所以先打印x的值,再执行0,所以第一个表达式的值为0,作为数组的第一个元素,以此类推直到最后一个元素

可变参数模版的应用emplace系列函数

在前面学习到的容器中,基本上都支持emplace系列的函数,以list类为例,list类中存在一个emplace_back函数,该函数可以支持在list的尾部插入数据,与push_back函数实现的功能基本一致,但是emplace_back函数除了可以支持插入已经构造的对象和单个用于构造对象的值,还可以接受构造函数的参数,直接在list的内存空间中调用该对象的构造函数,避免了额外的拷贝或移动操作。这可以提高效率,尤其是在处理复杂杂对象时,例如下面的代码:

int main()
{
    // 插入一个新的对象
    list<string> ls;
    string s1("1111111");
    ls.push_back(s1);
    ls.emplace_back(s1);

    // 插入一个右值
    ls.push_back("2222222");
    ls.emplace_back("2222222222");

    // 插入时构造对象
    list<pair<simulate_string::string, int>> ls1;
    // 插入时,用插入的内容构造一个pair对象
    ls.emplace_back("2222222", 2);

    return 0;
}

之所以可以接受构造函数的参数,是因为emplace_back函数本身是一个可变模版参数的函数模版,但是注意,这个可变参数模版不代表可以传递多个参数,例如,插入多个字符串ls.emplace_back("1111", "2222");这种行为是错误的

使用可变参数模版模拟实现emplace_back函数,以模拟实现list为例:

因为emplace_back本身是一个插入函数,所以底层调用insert函数即可,将函数的形式参数设置为右值引用,为了可以实现向下传递时也是右值引用,需要使用完美转发,代码如下:

// 模拟实现emplace_back
template<class... Args>
void emplace_back(Args&&... args)
{
    insert(end(), T(forward<Args>(args)...));
}

接着,实现insert函数针对emplace_back的版本,因为需要调用构造函数,所以当是右值引用时,需要保留是右值引用,同样需要使用完美转发

template<class... Args>
void insert(iterator pos, Args.., args)
{
    Node* cur = pos._node;
    Node* newnode = new Node(forward<Args>(x));// 保证右值引用不退化
    // ...
}

最后,完善Node节点的构造函数,使其满足可变参数模版,同样需要完美转发

template <class... Args>
ListNode(Args... args)
    : _next(nullptr)
    , _prev(nullptr)
    , _data(forward<Args>(args)...) // 保证右值引用不退化
{}

emplace系列函数和push系列函数的选择

以vector中的emplace_backpush_back为例

push_backemplace_back 都是 vector 类的成员函数,用于在 vector 的末尾添加元素。它们之间的主要区别在于添加元素的方式:

  1. push_back:接受一个已存在的对象作为参数,进行拷贝或移动,将其添加到 vector 的未尾。这会引发一次拷贝或移动构造函数的调用,具体取决于传递的对象是否可移动。
  2. emplace_back:接受构造函数的参数,直接在 vector 的内存空间中调用该对象的构造函数,避免了额外的拷贝或移动操作。这可以提高效率,尤其是在处理复杂对象时。

使用场景:

  • 如果需要将一个已经存在的对象添加到vector中,使用push_back
  • 如果希望直接在vector中构造对象,避免额外的拷贝或移动开销,使用emplace_back
当然可以无脑选择 emplace_back函数

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

怡晗★

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值