C++11:开启高效编程之旅(万字详解)

1.列表初始化

1.1. C++98传统的{}

C++98中⼀般数组和结构体可以⽤{}进行初始化。
struct Point
{
    int _x;
    int _y;
};

int main()
{
    //初始化数组
    int array1[] = { 1, 2, 3, 4, 5 };
    int array2[5] = { 0 };

    //初始化结构体
    Point p = { 1, 2 };

    return 0;
}

1.2. C++11中的{}

  • C++11以后想统⼀初始化⽅式,试图实现⼀切对象皆可⽤{}初始化,{}初始化也叫做列表初始化。
  • 内置类型⽀持,⾃定义类型也⽀持,⾃定义类型本质是类型转换,中间会产⽣临时对象,最后优化了以后变成直接构造。
  • {}初始化的过程中,可以省略掉= 。
  • C++11列表初始化的本意是想实现⼀个⼤统⼀的初始化⽅式,其次他在有些场景下带来的不少便利,如容器push/inset多参数构造的对象时,{}初始化会很⽅便。
#include
#include
using namespace std;

struct Point
{
    int _x;
    int _y;
};


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

    Date(const Date& d)
    :_year(d._year)
    , _month(d._month)
    , _day(d._day)
    {
        cout << "Date(const Date& d)" << endl;
    }

private:
    int _year;
    int _month;
    int _day;
};

int main()
{
    //内置类型的列表初始化
    int x1 = {1};
    int x2{2};
    
    //自定义类型的初始化

    // ⾃定义类型⽀持
    // 这⾥本质是⽤{2024, 1, 1}构造⼀个Date临时对象
    // 临时对象再去拷⻉构造d1,编译器优化后合⼆为⼀变成2024, 1, 1}直接构造初始化
    Date d1 = {2024, 1, 1};
    Date d2{2024, 1, 2};
    Date d3{2024};
    Date d4 = 2025;

    vector v;
    v.push_back(d1);
    v.push_back(Date(2025, 1, 1));

    // ⽐起有名对象和匿名对象传参,这⾥{}更有性价⽐
    v.push_back({ 2025, 1, 1 });

    return 0;
}

 1.3. C++11中的std::initializer_list

  • 上⾯的初始化已经很⽅便,但是对象容器初始化还是不太⽅便,⽐如⼀个vector对象,我想⽤N个值去构造初始化,那么我们得实现很多个构造函数才能⽀持, vector v1 = {1,2,3};vector v2 = {1,2,3,4,5}。
  • C++11库中提出了⼀个std::initializer_list的类, auto il = { 10, 20, 30 }; // the type of il is an initializer_list ,这个类的本质是底层开⼀个数组,将数据拷⻉过来std::initializer_list内部有两个指针分别指向数组的开始和结束。
  • std::initializer_list⽀持迭代器遍历。
  • 容器⽀持⼀个std::initializer_list的构造函数,也就⽀持任意多个值构成的 {x1,x2,x3...} 进⾏初始化。STL中的容器⽀持任意多个值构成的 {x1,x2,x3...} 进⾏初始化,就是通过
    std::initializer_list的构造函数⽀持的。
// STL中的容器都增加了⼀个initializer_list的构造
vector (initializer_list il, const allocator_type& alloc =
allocator_type());

list (initializer_list il, const allocator_type& alloc =
allocator_type());

map (initializer_list il,const key_compare& comp =
key_compare(),const allocator_type& alloc = allocator_type());
// ...

template
class vector {
public:
    typedef T* iterator;

    vector(initializer_list l)
    {
    for (auto e : l)
        push_back(e)
    }
private:
    iterator _start = nullptr;
    iterator _finish = nullptr;
    iterator _endofstorage = nullptr;
};

// 另外,容器的赋值也⽀持initializer_list的版本
vector& operator= (initializer_list il);
map& operator= (initializer_list il);

#include

#include
#include
#include

using namespace std;

int main()
{
    std::initializer_list mylist;
    mylist = { 10, 20, 30 };
    cout << sizeof(mylist) << endl;

    // 这⾥begin和end返回的值initializer_list对象中存的两个指针
    // 这两个指针的值跟i的地址跟接近,说明数组存在栈上
    int i = 0;
    cout << mylist.begin() << endl;
    cout << mylist.end() << endl;
    cout << &i << endl;

    // {}列表中可以有任意多个值
    // 这两个写法语义上还是有差别的,第⼀个v1是直接构造,
    // 第⼆个v2是构造临时对象+临时对象拷⻉v2+优化为直接构造
    vector v1({ 1,2,3,4,5 });
    vector v2 = { 1,2,3,4,5 };
    const vector& v3 = { 1,2,3,4,5 };

    // 这⾥是pair对象的{}初始化和map的initializer_list构造结合到⼀起⽤了
    map dict = { {"sort", "排序"}, {"string", "字符串"}};

    // initializer_list版本的赋值⽀持
    v1 = { 10,20,30,40,50 };

    return 0;
}

 2. 右值引用和移动语义

C++98的C++语法中就有引⽤的语法,⽽C++11中新增了的右值引⽤语法特性,C++11之后我们之前学习的引⽤就叫做左值引⽤。⽆论左值引⽤还是右值引⽤,都是给对象取别名。

2.1. 左值和右值

  • 左值是⼀个表⽰数据的表达式(如变量名或解引⽤的指针),⼀般是有持久状态,存储在内存中,我们可以获取它的地址,左值可以出现赋值符号的左边,也可以出现在赋值符号右边。定义时const修饰符后的左值,不能给他赋值,但是可以取它的地址。
  • 右值也是⼀个表⽰数据的表达式,要么是字⾯值常量、要么是表达式求值过程中创建的临时对象等,右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边,右值不能取地址。
  • 值得⼀提的是,左值的英⽂简写为lvalue,右值的英⽂简写为rvalue。传统认为它们分别是leftvalue、right value 的缩写。现代C++中,lvalue 被解释为loactor value的缩写,可意为存储在内存中、有明确存储地址可以取地址的对象,⽽ rvalue 被解释为 read value,指的是那些可以提供数据值,但是不可以寻址,例如:临时变量,字⾯量常量,存储于寄存器中的变量等,也就是说左值和右值的核⼼区别就是能否取地址。
#include
using namespace std;

int main()
{
    // 左值:可以取地址
    // 以下的p、b、c、*p、s、s[0]就是常⻅的左值
    int* p = new int(0);
    int b = 1;
    const int c = b;
    *p = 10;
    string s("111111");
    s[0] = 'x';

    cout << &c << endl;
    cout << (void*)&s[0] << endl;

    // 右值:不能取地址
    double x = 1.1, y = 2.2;
    // 以下⼏个10、x + y、fmin(x, y)、string("11111")都是常⻅的右值
    10;
    x + y;
    fmin(x, y);
    string("11111");
    
    //程序无法对右值属性的对象取地址,程序报错
    //cout << &10 << endl;
    //cout << &(x+y) << endl;
    //cout << &(fmin(x, y)) << endl;
    //cout << &string("11111") << endl;

    return 0;
}

2.2. 左值引用和右值引用

  • Type& r1 = x; Type&& rr1 = y; 第⼀个语句就是左值引⽤,左值引⽤就是给左值取别
    名,第⼆个就是右值引⽤,同样的道理,右值引⽤就是给右值取别名。
  • template typename remove_reference::type&& move (T&& arg)
  • move是库⾥⾯的⼀个函数模板,本质内部是进⾏强制类型转换。
  • 需要注意的是变量表达式都是左值属性,也就意味着⼀个右值被右值引⽤绑定后,右值引⽤变量变量表达式的属性是左值
  • 语法层⾯看,左值引⽤和右值引⽤都是取别名,不开空间。从汇编底层的⻆度看下⾯代码中r1和rr1汇编层实现,底层都是⽤指针实现的,没什么区别。底层汇编等实现和上层语法表达的意义有时是背离的,所以不要然到⼀起去理解,互相佐证,这样反⽽是陷⼊迷途。
template 
remove_reference_t<_Ty>&& move(_Ty&& _Arg)
{     // forward _Arg as movable
      return static_cast&&>(_Arg);
}


#include
using namespace std;

int main()
{

    // 左值:可以取地址
    // 以下的p、b、c、*p、s、s[0]就是常⻅的左值
    int* p = new int(0);
    int b = 1;
    const int c = b;
    *p = 10;
    string s("111111");
    s[0] = 'x';
    double x = 1.1, y = 2.2;

    // 左值引⽤给左值取别名
    int& r1 = b;
    int*& r2 = p;
    int& r3 = *p;
    string& r4 = s;
    char& r5 = s[0];

    // 右值引⽤给右值取别名
    // 右值带有常性
    int&& rr1 = 10;
    double&& rr2 = x + y;
    double&& rr3 = fmin(x, y);
    string&& rr4 = string("11111");

    // 左值引⽤不能直接引⽤右值,但是const左值引⽤可以引⽤右值
    const int& rx1 = 10;
    const double& rx2 = x + y;
    const double& rx3 = fmin(x, y);
    const string& rx4 = string("11111");

    // 右值引⽤不能直接引⽤左值,但是右值引⽤可以引⽤move(左值)
    int&& rrx1 = move(b);
    int*&& rrx2 = move(p);
    int&& rrx3 = move(*p);
    string&& rrx4 = move(s);
    
    //move() 的本质是强制类型转化
    string&& rrx5 = (string&&)s;
    
    // b、r1、rr1都是变量表达式,都是左值
    cout << &b << endl;
    cout << &r1 << endl;
    cout << &rr1 << endl;

    // 这⾥要注意的是,rr1的属性是左值,所以不能再被右值引⽤绑定,除⾮move⼀下
    // 右值引用 引用 右值,那么右值引用是左值属性
    int& r6 = r1;

    // int&& rrx6 = rr1;
    int&& rrx6 = move(rr1);

    return 0;
}

 2.3. 引用延长生命周期

右值引⽤可⽤于为临时对象延⻓⽣命周期,const 的左值引⽤也能延⻓临时对象⽣存期,但这些对象⽆法被修改。
int main()
{
    std::string s1 = "Test";
    // std::string&& r1 = s1; // 错误:不能绑定到左值

    //右值引用引用临时对象和匿名对象延长其生命周期
    const std::string& r2 = s1 + s1; // OK:到 const 的左值引⽤延⻓⽣存期
    // r2 += "Test"; // 错误:不能通过到 const 的引⽤修改

    std::string&& r3 = s1 + s1; // OK:右值引⽤延⻓⽣存期
    r3 += "Test"; // OK:能通过到⾮ const 的引⽤修改

    std::cout << r3 << '\n';

    return 0;
}

2.4. 左值和右值的参数匹配

  • C++98中,我们实现⼀个const左值引⽤作为参数的函数,那么实参传递左值和右值都可以匹配。
  • C++11以后,分别重载左值引⽤、const左值引⽤、右值引⽤作为形参的f函数,那么实参是左值会匹配f(左值引⽤),实参是const左值会匹配f(const 左值引⽤),实参是右值会匹配f(右值引⽤)。
  • 右值引⽤变量在⽤于表达式时属性是左值,这个设计这⾥会感觉跟怪,下⼀⼩节我们讲右值引⽤的使⽤场景时,就能体会这样设计的价值了。 
#include

using namespace std;

void f(int& x)
{
    cout<<"左值引用重载"<

2.5. 右值引用和移动语义的使用场景

2.5.1. 左值引用主要使用场景

左值引⽤主要使⽤场景是在函数中左值引⽤传参和左值引⽤传返回值时减少拷⻉,同时还可以修改实参和修改返回对象的价值。左值引⽤已经解决⼤多数场景的拷⻉效率问题,但是有些场景不能使⽤传左值引⽤返回,如addStrings和generate函数,C++98中的解决⽅案只能是被迫使⽤输出型参数解决。那么C++11以后这⾥可以使⽤右值引⽤做返回值解决吗?显然是不可能的,因为这⾥的本质是返回对象是⼀个局部对象,函数结束这个对象就析构销毁了,右值引⽤返回也⽆法概念对象已经析构销毁的事实。
class Solution {
public:
// 传值返回需要拷⻉
    string addStrings(string num1, string num2) 
    {
        string str;
        int end1 = num1.size()-1, end2 = num2.size()-1;
        // 进位
        int next = 0;
        while(end1 >= 0 || end2 >= 0)
        {
            int val1 = end1 >= 0 ? num1[end1--]-'0' : 0;
            int val2 = end2 >= 0 ? num2[end2--]-'0' : 0;
            int ret = val1 + val2+next;
            next = ret / 10;
            ret = ret % 10;
            str += ('0'+ret);
        }
        if(next == 1)
            str += '1';

        reverse(str.begin(), str.end());
        return str;
    }
};


class Solution {
public:
// 这⾥的传值返回拷⻉代价就太⼤了
vector> generate(int numRows) 
    {
        vector> vv(numRows);
        for(int i = 0; i < numRows; ++i)
        {
            vv[i].resize(i+1, 1);
        }
        for(int i = 2; i < numRows; ++i)
        {
            for(int j = 1; j < i; ++j)
            {
                vv[i][j] = vv[i-1][j] + vv[i-1][j-1];
            }
        }

        return vv;
    }
};

 2.5.2. 移动构造和移动赋值

  • 移动构造函数是⼀种构造函数,类似拷⻉构造函数,移动构造函数要求第⼀个参数是该类类型的引⽤,但是不同的是要求这个参数是右值引⽤,如果还有其他参数,额外的参数必须有缺省值。
  • 移动赋值是⼀个赋值运算符的重载,他跟拷⻉赋值构成函数重载,类似拷⻉赋值函数,移动赋值函数要求第⼀个参数是该类类型的引⽤,但是不同的是要求这个参数是右值引⽤。
  • 对于像string/vector这样的深拷⻉的类或者包含深拷⻉的成员变量的类,移动构造和移动赋值才有意义,因为移动构造和移动赋值的第⼀个参数都是右值引⽤的类型,他的本质是要“窃取”引⽤的右值对象的资源,⽽不是像拷⻉构造和拷⻉赋值那样去拷⻉资源,从提⾼效率。下⾯的bit::string样例实现了移动构造和移动赋值,我们需要结合场景理解。
#define _CRT_SECURE_NO_WARNINGS 1
#include
#include
#include
#include

using namespace std;

namespace bit
{
class string
{
public:
    typedef char* iterator;
    typedef const char* const_iterator;
    iterator begin()
    {
        return _str;
    }

    iterator end()
    {
        return _str + _size;
    }

    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);
    }

    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(string&& s)
    {
        cout << "string(string&& s) -- 移动构造" << endl;
        swap(s);
    }

    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& operator=(string&& s)
    {
        cout << "string& operator=(string&& s) -- 移动赋值" << endl;
        swap(s);
        return *this;
    }

    ~string()
    {
        cout << "~string() -- 析构" << endl;
        delete[] _str;
        _str = nullptr;
    }

    char& operator[](size_t pos)
    {
        assert(pos < _size);
        return _str[pos];
    }

    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)
    {
        push_back(ch);
        return *this;
    }

    const char* c_str() const
    {
        return _str;
    }

    size_t size() const
    {
        return _size;
    }

private:
    char* _str = nullptr;
    size_t _size = 0;
    size_t _capacity = 0;
};
}   

int main()
{
    bit::string s1("xxxxx");
    // 拷⻉构造
    bit::string s2 = s1;
    // 构造+移动构造,优化后直接构造
    bit::string s3 = bit::string("yyyyy");
    // 移动构造
    bit::string s4 = move(s1);
    cout << "******************************" << endl;

    return 0;
} 
    

 2.5.3. 右值引用和移动语义解决传值返回问题

namespace bit
{
    string addStrings(string num1, string num2)
    {
        string str;
        int end1 = num1.size() - 1, end2 = num2.size() - 1;
        int next = 0;
        while (end1 >= 0 || end2 >= 0)
        {
            int val1 = end1 >= 0 ? num1[end1--] - '0' : 0;
            int val2 = end2 >= 0 ? num2[end2--] - '0' : 0;

            int ret = val1 + val2 + next;
            next = ret / 10;
            ret = ret % 10;

            str += ('0' + ret);
        }

        if (next == 1)
            str += '1';

    reverse(str.begin(), str.end());
    cout << "******************************" << endl;

    return str;
    }
}  

 // 场景1
int main()
{
    bit::string ret = bit::addStrings("11111", "2222");
    cout << ret.c_str() << endl;

    return 0;
} 

// 场景2
int main()
{
    bit::string ret;
    ret = bit::addStrings("11111", "2222");
    cout << ret.c_str() << endl;

    return 0;
}

2.5.4. 右值引用和移动语义在传参中的提效

  • 查看STL⽂档我们发现C++11以后容器的push和insert系列的接⼝否增加的右值引⽤版本
  • 当实参是⼀个左值时,容器内部继续调⽤拷⻉构造进⾏拷⻉,将对象拷⻉到容器空间中的对象
  • 当实参是⼀个右值,容器内部则调⽤移动构造,右值对象的资源到容器空间的对象上
  • 其实这⾥还有⼀个emplace系列的接⼝,但是这个涉及可变参数模板,文章后面的部分回提及

 2.6. 类型分类

  • C++11以后,进⼀步对类型进⾏了划分,右值被划分纯右值(pure value,简称prvalue)和将亡值 (expiring value,简称xvalue)。
  • 纯右值是指那些字⾯值常量或求值结果相当于字⾯值或是⼀个不具名的临时对象。如: 42 、true、 nullptr 或者类似 str.substr(1, 2) str1 + str2 传值返回函数调⽤,或者整形 a b a++ a+b 等。纯右值和将亡值C++11中提出的,C++11中的纯右值概念划分等价于 C++98中的右值。
  • 将亡值是指返回右值引⽤的函数的调⽤表达式和转换为右值引⽤的转换函数的调⽤表达,如 move(x)、 static_cast(x)
  • 泛左值(generalized value,简称glvalue),泛左值包含将亡值和左值。

 2.7. 引用折叠

  • C++中不能直接定义引⽤的引⽤如 int& && r = i; ,这样写会直接报错,通过模板的typedef 中的类型操作可以构成引⽤的引⽤。
  • 通过模板或 typedef 中的类型操作可以构成引⽤的引⽤时,这时C++11给出了⼀个引⽤折叠的规则:右值引⽤的右值引⽤折叠成右值引⽤,所有其他组合均折叠成左值引⽤。
  • 下⾯的程序中很好的展⽰了模板和typedef时构成引⽤的引⽤时的引⽤折叠规则,⼤家需要⼀个⼀个仔细理解⼀下。
  • 像f2这样的函数模板中,T&& x参数看起来是右值引⽤参数,但是由于引⽤折叠的规则,他传递左值时就是左值引⽤,传递右值时就是右值引⽤,有些地⽅也把这种函数模板的参数叫做万能引⽤
  • Function(T&& t)函数模板程序中,假设实参是int右值,模板参数T的推导int,实参是int左值,模板参数T的推导int&,再结合引⽤折叠规则,就实现了实参是左值,实例化出左值引⽤版本形参的Function,实参是右值,实例化出右值引⽤版本形参的Function。

 总的来说:左值引用 + 左值引用 = 左值引用左值引用 + 右值引用 = 左值引用

                   右值引用 + 左值引用 = 左值引用,右值引用 +  右值引用 = 右值引用    

// 由于引⽤折叠限定,f1实例化以后总是⼀个左值引⽤
template
void f1(T& x)
{}

// 由于引⽤折叠限定,f2实例化后可以是左值引⽤,也可以是右值引⽤
template
void f2(T&& x)
{}

int main()
{
    typedef int& lref;
    typedef int&& rref;
    int n = 0;

    lref& r1 = n; // r1 的类型是 int&
    lref&& r2 = n; // r2 的类型是 int&
    rref& r3 = n; // r3 的类型是 int&
    rref&& r4 = 1; // r4 的类型是 int&&

    // 没有折叠->实例化为void f1(int& x)
    f1(n);
    f1(0); // 报错

    // 折叠->实例化为void f1(int& x)
    f1(n);
    f1(0); // 报错

    // 折叠->实例化为void f1(int& x)
    f1(n);
    f1(0); // 报错

    // 折叠->实例化为void f1(const int& x)
    f1(n);
    f1(0);

    // 折叠->实例化为void f1(const int& x)
    f1(n);
    f1(0);

    // 没有折叠->实例化为void f2(int&& x)
    f2(n); // 报错
    f2(0);

    // 折叠->实例化为void f2(int& x)
    f2(n);
    f2(0); // 报错

    // 折叠->实例化为void f2(int&& x)
    f2(n); // 报错
    f2(0);

    return 0;
}

2.8. 完美转发

  • Function(T&& t)函数模板程序中,传左值实例化以后是左值引⽤的Function函数,传右值实例化以后是右值引⽤的Function函数。
  • 但是结合之前的的讲解,变量表达式都是左值属性,也就意味着⼀个右值被右值引⽤绑定后,右值引⽤变量表达式的属性是左值,也就是说Function函数中t的属性是左值,那么我们把t传递给下⼀层函数Fun,那么匹配的都是左值引⽤版本的Fun函数。这⾥我们想要保持t对象的属性,就需要使⽤完美转发实现。
  • template T&& forward (typename remove_reference::type& arg);
  • template T&& forward (typename remove_reference::type&& arg);
  • 完美转发forward本质是⼀个函数模板,他主要还是通过引⽤折叠的⽅式实现,下⾯⽰例中传递给Function的实参是右值,T被推导为int,没有折叠,forward内部t被强转为右值引⽤返回;传递给Function的实参是左值,T被推导为int&,引⽤折叠为左值引⽤,forward内部t被强转为左值引⽤返回。
template 
_Ty&& forward(remove_reference_t<_Ty>& _Arg) noexcept
{     // forward an lvalue as either an lvalue or an rvalue
      return static_cast<_Ty&&>(_Arg);
}

void Fun(int& x) { cout << "左值引⽤" << endl; }
void Fun(const int& x) { cout << "const 左值引⽤" << endl; }

void Fun(int&& x) { cout << "右值引⽤" << endl; }
void Fun(const int&& x) { cout << "const 右值引⽤" << endl; }

template
void Function(T&& t)
{
    Fun(t);
    //Fun(forward(t));
}

int main()
{
    // 10是右值,推导出T为int,模板实例化为void Function(int&& t)
    Function(10); // 右值

    int a;
    // a是左值,推导出T为int&,引⽤折叠,模板实例化为void Function(int& t)
    Function(a); // 左值

    // std::move(a)是右值,推导出T为int,模板实例化为void Function(int&& t)
    Function(std::move(a)); // 右值

    const int b = 8;
    // a是左值,推导出T为const int&,引⽤折叠,模板实例化为void Function(const int& t)
    Function(b); // const 左值

    // std::move(b)右值,推导出T为const int,模板实例化为void Function(const int&& t)
    Function(std::move(b)); // const 右值

    return 0;
}

运行结果如下:

C++11:开启高效编程之旅(万字详解)_第1张图片 C++11:开启高效编程之旅(万字详解)_第2张图片

3. 可变模板参数

3.1. 基本语法及理解

  • C++11⽀持可变参数模板,也就是说⽀持可变数量参数的函数模板和类模板,可变数⽬的参数被称为参数包,存在两种参数包:模板参数包,表⽰零或多个模板参数;函数参数包:表⽰零或多个函数参数。
  • template void Func(Args... args) {}
  • template void Func(Args&... args) {}
  • template void Func(Args&&... args) {}
  • 我们⽤省略号来指出⼀个模板参数或函数参数的表⽰⼀个包,在模板参数列表中,class...或 typename...指出接下来的参数表⽰零或多个类型列表;在函数参数列表中,类型名后⾯跟...指出接下来表⽰零或多个形参对象列表;函数参数包可以⽤左值引⽤或右值引⽤表⽰,跟前⾯普通模板⼀样,每个参数实例化时遵循引⽤折叠规则。
  • 可变参数模板的原理跟模板类似,本质还是去实例化对应类型和个数的多个函数。
  • 这⾥我们可以使⽤sizeof...运算符去计算参数包中参数的个数。
template 
void Print(Args&&... args)
{
    cout << sizeof...(args) << endl;
}

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

    return 0;
}

// 原理1:编译本质这⾥会结合引⽤折叠规则实例化出以下四个函数
void Print();
void Print(int&& arg1);
void Print(int&& arg1, string&& arg2);
void Print(double&& arg1, string&& arg2, double& arg3);

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

template 
void Print(T1&& arg1);

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

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

 3.2. 包扩展

  • 对于⼀个参数包,我们除了能计算他的参数个数,我们能做的唯⼀的事情就是扩展它,当扩展⼀个包时,我们还要提供⽤于每个扩展元素的模式,扩展⼀个包就是将它分解为构成的元素,对每个元素应⽤模式,获得扩展后的列表。我们通过在模式的右边放⼀个省略号(...)来触发扩展操作。
  • C++还⽀持更复杂的包扩展,直接将参数包依次展开依次作为实参给⼀个函数去处理。
void ShowList()
{
    // 编译器时递归的终⽌条件,参数包是0个时,直接匹配这个函数
    cout << endl;
}

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

// 编译时递归推导解析参数
template 
void Print(Args... args)
{
    ShowList(args...);
}

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

    return 0;
}

 3.3. emplace系列接口

  • template void emplace_back (Args&&... args);
  • template iterator emplace (const_iterator position, Args&&... args);
  • C++11以后STL容器新增了empalce系列的接⼝,empalce系列的接⼝均为模板可变参数,功能上兼容push和insert系列,但是empalce还⽀持新玩法,假设容器为container,empalce还⽀持直接插⼊构造T对象的参数,这样有些场景会更⾼效⼀些,可以直接在容器空间上构造T对象。
  • emplace_back总体⽽⾔是更⾼效,推荐以后使⽤emplace系列替代insert和push系列
  • 传递参数包过程中,如果是 Args&&... args 的参数包,要⽤完美转发参数包,⽅式如下
    std::forward(args)... ,否则编译时包扩展后右值引⽤变量表达式就变成了左值。

4. 新的类功能

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

  •  原来C++类中,有6个默认成员函数:构造函数/析构函数/拷⻉构造函数/拷⻉赋值重载/取地址重载/const 取地址重载,最后重要的是前4个,后两个⽤处不⼤,默认成员函数就是我们不写编译器会⽣成⼀个默认的。C++11 新增了两个默认成员函数,移动构造函数和移动赋值运算符重载。
  • 如果你没有⾃⼰实现移动构造函数,且没有实现析构函数 、拷⻉构造、拷⻉赋值重载中的任意⼀个。那么编译器会⾃动⽣成⼀个默认移动构造。默认⽣成的移动构造函数,对于内置类型成员会执⾏逐成员按字节拷⻉,⾃定义类型成员,则需要看这个成员是否实现移动构造,如果实现了就调⽤移动构造,没有实现就调⽤拷⻉构造。
  • 如果你没有⾃⼰实现移动赋值重载函数,且没有实现析构函数 、拷⻉构造、拷⻉赋值重载中的任意⼀个,那么编译器会⾃动⽣成⼀个默认移动赋值。默认⽣成的移动构造函数,对于内置类型成员会执⾏逐成员按字节拷⻉,⾃定义类型成员,则需要看这个成员是否实现移动赋值,如果实现了就调⽤移动赋值,没有实现就调⽤拷⻉赋值。(默认移动赋值跟上⾯移动构造完全类似)
  • 如果你提供了移动构造或者移动赋值,编译器不会⾃动提供拷⻉构造和拷⻉赋值。
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& operator=(const Person& p)
    {
        if(this != &p)
        {
            _name = p._name;
            _age = p._age;
        }

        return *this;
    }*/

    /*~Person()
    {}*/

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

int main()
{
    Person s1;
    Person s2 = s1;
    Person s3 = std::move(s1);
    Person s4;
    s4 = std::move(s2);

    return 0;
}

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

  • 成员变量声明时给缺省值是给初始化列表⽤的,如果没有显⽰在初始化列表初始化,就会在初始化列表⽤这个缺省值初始化。
class Date
{
public:
    Date()
    {}

private:
    int _year = 2025;
    int _month = 1;
    int _day = 1;
}

 4.3. default和delete

  • C++11可以让你更好的控制要使⽤的默认函数。假设你要使⽤某个默认的函数,但是因为⼀些原因这个函数没有默认⽣成。⽐如:我们提供了拷⻉构造,就不会⽣成移动构造了,那么我们可以使⽤default关键字显⽰指定移动构造⽣成。
  • 如果能想要限制某些默认函数的⽣成,在C++98中,是该函数设置成private,这样只要其他⼈想要调⽤就会报错。在C++11中更简单,只需在该函数声明加上=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:
    bit::string _name;
    int _age;
};

int main()
{
    Person s1;
    Person s2 = s1;
    Person s3 = std::move(s1);

    return 0;
}

 5.STL中的一些变化

  • STL中新加了几个容器,但是实际最有⽤的是unordered_map和unordered_set。这两个我们前⾯已经进⾏了⾮常详细的讲解,其他的了解⼀下即可。
  • STL中容器的新接⼝也不少,最重要的就是右值引⽤和移动语义相关的push/insert/emplace系列接⼝和移动构造和移动赋值,还有initializer_list版本的构造等。
  • 容器的范围for遍历。

6. lambda

6.1. lambda表达式语法

  •  lambda 表达式本质是⼀个匿名函数对象,跟普通函数不同的是他可以定义在函数内部。lambda 表达式语法使⽤层⽽⾔没有类型,所以我们⼀般是⽤auto或者模板参数定义的对象去接lambda 对象。
  • lambda表达式的格式: [capture-list] (parameters)-> return type { function boby }
  • [capture-list] : 捕捉列表,该列表总是出现在 lambda 函数的开始位置,编译器根据[]来
    判断接下来的代码是否为 lambda 函数,捕捉列表能够捕捉上下⽂中的变量供 lambda 函数使⽤,捕捉列表可以传值和传引⽤捕捉,具体细节后面会讲解。捕捉列表为空也不能省略。
  • ->return type :返回值类型,⽤追踪返回类型形式声明函数的返回值类型,没有返回值时此部分可省略。⼀般返回值类型明确情况下,也可省略,由编译器对返回类型进⾏推导。
  • {function boby} :函数体,函数体内的实现跟普通函数完全类似,在该函数体内,除了可以使⽤其参数外,还可以使⽤所有捕获到的变量,函数体为空也不能省略。
int main()
{
    // ⼀个简单的lambda表达式
    auto add1 = [](int x, int y)->int {return x + y; };
    cout << add1(1, 2) << endl;

    // 1、捕捉为空也不能省略
    // 2、参数为空可以省略
    // 3、返回值可以省略,可以通过返回对象⾃动推导
    // 4、函数体不能省略
    auto func1 = []
    {
        cout << "hello world" << endl;
        return 0;
    };

    func1();

    int a = 0, b = 1;
    auto swap1 = [](int& x, int& y)
    {
        int tmp = x;
        x = y;
        y = tmp;
    };
    swap1(a, b);
    cout << a << ":" << b << endl;

    return 0;
}
    

 6.2. 捕捉列表

  • lambda 表达式中默认只能⽤ lambda 函数体和参数中的变量,如果想⽤外层作⽤域中的变量就需要进⾏捕捉。
  • 第⼀种捕捉⽅式是在捕捉列表中显⽰的传值捕捉和传引⽤捕捉,捕捉的多个变量⽤逗号分割。[x,y,&z] 表⽰x和y值捕捉,z引⽤捕捉。
  • 第⼆种捕捉⽅式是在捕捉列表中隐式捕捉,我们在捕捉列表写⼀个=表⽰隐式值捕捉,在捕捉列表写⼀个&表⽰隐式引⽤捕捉,这样我们 lambda 表达式中⽤了那些变量,编译器就会⾃动捕捉那些变量。
  • 第三种捕捉⽅式是在捕捉列表中混合使⽤隐式捕捉和显⽰捕捉。[=, &x]表⽰其他变量隐式值捕捉,x引⽤捕捉;[&, x, y]表⽰其他变量引⽤捕捉,x和y值捕捉。当使⽤混合捕捉时,第⼀个元素必须是&或=,并且&混合捕捉时,后⾯的捕捉变量必须是值捕捉,同理=混合捕捉时,后⾯的捕捉变量必须是引⽤捕捉。
  • lambda 表达式如果在函数局部域中,他可以捕捉 lambda 位置之前定义的变量,不能捕捉静态局部变量和全局变量,静态局部变量和全局变量也不需要捕捉, lambda 表达式中可以直接使⽤。这也意味着 lambda 表达式如果定义在全局位置,捕捉列表必须为空。
  • 默认情况下, lambda 捕捉列表是被const修饰的,也就是说传值捕捉的过来的对象不能修改,mutable加在参数列表的后⾯可以取消其常量性,也就说使⽤该修饰符后,传值捕捉的对象就可以修改了,但是修改还是形参对象,不会影响实参。使⽤该修饰符后,参数列表不可省略(即使参数为空)。
int x = 0;
// 捕捉列表必须为空,因为全局变量不⽤捕捉就可以⽤,没有可被捕捉的变量
auto func1 = []()
{
    x++;
};

int main()
{
    // 只能⽤当前lambda局部域和捕捉的对象和全局对象
    int a = 0, b = 1, c = 2, d = 3;
    auto func1 = [a, &b]
    {
        // 值捕捉的变量不能修改,引⽤捕捉的变量可以修改
        //a++;
        b++;
        int ret = a + b;

        return ret;
    };
    cout<

6.3.lambda的应用

  • 在学习 lambda 表达式之前,我们的使⽤的可调⽤对象只有函数指针和仿函数对象,函数指针的类型定义起来⽐较⿇烦,仿函数要定义⼀个类,相对会⽐较⿇烦。使⽤ lambda 去定义可调⽤对象,既简单⼜⽅便。
  • lambda 在很多其他地⽅⽤起来也很好⽤。⽐如线程中定义线程的执⾏函数逻辑,智能指针中定制删除器等, lambda 的应⽤还是很⼴泛的。
struct Goods
{
    string _name; // 名字
    double _price; // 价格
    int _evaluate; // 评价
    // ...

    Goods(const char* str, double price, int evaluate)
        :_name(str)
        , _price(price)
        , _evaluate(evaluate)
    {}
};

struct ComparePriceLess
{
    bool operator()(const Goods& gl, const Goods& gr)
    {
        return gl._price < gr._price;
    }
};

struct ComparePriceGreater
{
    bool operator()(const Goods& gl, const Goods& gr)
    {
            return gl._price > gr._price;
    }
};

int main()
{
    vector v = { { "苹果", 2.1, 5 }, { "⾹蕉", 3, 4 }, { "橙⼦", 2.2, 3}, 
                        { "菠萝", 1.5, 4 } };
    // 类似这样的场景,我们实现仿函数对象或者函数指针⽀持商品中
    // 不同项的⽐较,相对还是⽐较⿇烦的,那么这⾥lambda就很好⽤了
    sort(v.begin(), v.end(), ComparePriceLess());
    sort(v.begin(), v.end(), ComparePriceGreater());

    sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) {
    return g1._price < g2._price;});

    sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) {
    return g1._price > g2._price;});

    sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) {
    return g1._evaluate < g2._evaluate;});

    sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) {
    return g1._evaluate > g2._evaluate;});

    return 0;
}

7. 包装器

7.1. function

template 
class function; // undefined

template 
class function;
  • std::function 是⼀个类模板,也是⼀个包装器。 std::function 的实例对象可以包装存储其他的可以调⽤对象,包括函数指针、仿函数、 lambda bind 表达式等,存储的可调⽤对象被称为 std::function ⽬标 。若 std::function 不含⽬标,则称它为 。调⽤ std::function 的 ⽬标 导致抛出 std::bad_function_call 异常。
  • 以上是 function 的原型,他被定义头⽂件中。 std::function -cppreference.com 是function的官⽅⽂件链接。
  • 函数指针、仿函数、 lambda 等可调⽤对象的类型各不相同, std::function 的优势就是统 ⼀类型,对他们都可以进⾏包装,这样在很多地⽅就⽅便声明可调⽤对象的类型,
    下⾯的第⼆个代码样例展⽰了 std::function 作为map的参数,实现字符串和可调⽤对象的映射表功能。
#include

int f(int a, int b)
{
    return a + b;
}

struct Functor
{
public:
    int operator() (int a, int b)
    {
        return a + b;
    }
};

class Plus
{
public:
    Plus(int n = 10)
        :_n(n)
    {}

    static int plusi(int a, int b)
    {
        return a + b;
    }

    double plusd(double a, double b)
    {
        return (a + b) * _n;
    }

private:
    int _n;
};

int main()
{
    // 包装各种可调⽤对象
    function f1 = f;
    function f2 = Functor();
    function f3 = [](int a, int b) {return a + b; };

    cout << f1(1, 1) << endl;
    cout << f2(1, 1) << endl;
    cout << f3(1, 1) << endl;

    // 包装静态成员函数
    // 成员函数要指定类域并且前⾯加&才能获取地址
    function f4 = &Plus::plusi;
    cout << f4(1, 1) << endl;

    // 包装普通成员函数
    // 普通成员函数还有⼀个隐含的this指针参数,所以绑定时传对象或者对象的指针过去都可以
    function f5 = &Plus::plusd;
    Plus pd;
    cout << f5(&pd, 1.1, 1.1) << endl;

    function f6 = &Plus::plusd;
    cout << f6(pd, 1.1, 1.1) << endl;
    cout << f6(pd, 1.1, 1.1) << endl;

    function f7 = &Plus::plusd;
    cout << f7(move(pd), 1.1, 1.1) << endl;
    cout << f7(Plus(), 1.1, 1.1) << endl;

    return 0;
}

 7.2. bind

simple(1)
template 
/* unspecified */ bind (Fn&& fn, Args&&... args);

with return type (2)
template 
/* unspecified */ bind (Fn&& fn, Args&&... args);

  • bind 是⼀个函数模板,它也是⼀个可调⽤对象的包装器,可以把他看做⼀个函数适配器,对接收的fn可调⽤对象进⾏处理后返回⼀个可调⽤对象。 bind 可以⽤来调整参数个数和参数顺序。bind 也在这个头⽂件中。
  • 调⽤bind的⼀般形式: auto newCallable = bind(callable,arg_list); 其中newCallable本⾝是⼀个可调⽤对象,arg_list是⼀个逗号分隔的参数列表,对应给定的callable的参数。当我们调⽤newCallable时,newCallable会调⽤callable,并传给它arg_list中的参数。
  • arg_list中的参数可能包含形如_n的名字,其中n是⼀个整数,这些参数是占位符,表⽰
    newCallable的参数,它们占据了传递给newCallable的参数的位置。数值n表⽰⽣成的可调⽤对象中参数的位置:_1为newCallable的第⼀个参数,_2为第⼆个参数,以此类推。_1/_2/_3....这些占位符放到placeholders的⼀个命名空间中。
#include

using placeholders::_1;
using placeholders::_2;
using placeholders::_3;

int Sub(int a, int b)
{
    return (a - b) * 10;
}

int SubX(int a, int b, int c)
{
    return (a - b - c) * 10;
}

class Plus
{
public:
    static int plusi(int a, int b)
    {
        return a + b;
    }

    double plusd(double a, double b)
    {
        return a + b;
    }
};

int main()
{
    auto sub1 = bind(Sub, _1, _2);
    cout << sub1(10, 5) << endl;
    // bind 本质返回的⼀个仿函数对象
    // 调整参数顺序(不常⽤)
    // _1代表第⼀个实参
    // _2代表第⼆个实参
    // ...
    auto sub2 = bind(Sub, _2, _1);
    cout << sub2(10, 5) << endl;

    // 调整参数个数 (常⽤)
    auto sub3 = bind(Sub, 100, _1);
    cout << sub3(5) << endl;

    auto sub4 = bind(Sub, _1, 100);
    cout << sub4(5) << endl;

    // 分别绑死第123个参数
    auto sub5 = bind(SubX, 100, _1, _2);
    cout << sub5(5, 1) << endl;

    auto sub6 = bind(SubX, _1, 100, _2);
    cout << sub6(5, 1) << endl;

    auto sub7 = bind(SubX, _1, _2, 100);
    cout << sub7(5, 1) << endl;

    // 成员函数对象进⾏绑死,就不需要每次都传递了
    function f6 = &Plus::plusd;
    Plus pd;
    cout << f6(move(pd), 1.1, 1.1) << endl;
    cout << f6(Plus(), 1.1, 1.1) << endl;

    // bind⼀般⽤于,绑死⼀些固定参数
    function f7 = bind(&Plus::plusd, Plus(), _1, _2);
    cout << f7(1.1, 1.1) << endl;

    // 计算复利的lambda
    auto func1 = [](double rate, double money, int year)->double {
        double ret = money;
        for (int i = 0; i < year; i++)
        {
            ret += ret * rate;
        }
        return ret - money;
    };
    // 绑死⼀些参数,实现出⽀持不同年华利率,不同⾦额和不同年份计算出复利的结算利息
    function func3_1_5 = bind(func1, 0.015, _1, 3);
    function func5_1_5 = bind(func1, 0.015, _1, 5);
    function func10_2_5 = bind(func1, 0.025, _1, 10);
    function func20_3_5 = bind(func1, 0.035, _1, 30);

    cout << func3_1_5(1000000) << endl;
    cout << func5_1_5(1000000) << endl;
    cout << func10_2_5(1000000) << endl;
    cout << func20_3_5(1000000) << endl;

    return 0;
}

 

你可能感兴趣的:(c++,开发语言)