【C++】C++11右值引用|新增默认成员函数|可变参数模版|lambda表达式(中)

简介: 【C++】C++11右值引用|新增默认成员函数|可变参数模版|lambda表达式(中)

1.5 完美转发

1.5.1 万能引用

我们上面都是单独定义一个参数为右值引用的函数,然后让编译器根据实参的类型来选择调用参数为左值引用的构造/插入接口还是参数为右值引用的构造/插入接口。那么,我们能不能让函数能够根据实参的类型自动实例化出对应不同的函数呢?


万能引用可以实现这个功能。


所谓的万能引用,实际上是一个模板,且函数的形参为右值引用。对于这种模板,编译器能够根据实参的类型自动推衍实例化出不同的形参类型,这个模板能够接收左值/const左值/右值/const右值,推衍出的类型为:左值引用/const左值引用/右值引用/const右值引用。


举个例子

template<class T>
void PerfectForward(T&& x)
{
    cout << "void PerfectForward(int&& x)" << endl;
}
void Test7()
{
    PerfectForward(10); // 右值
    int a = 10;
    PerfectForward(a); //左值
    const int b = 8;
    PerfectForward(b); //const 左值
    PerfectForward(std::move(b)); //const右值
}


四个变量同时调用PerfectForward,都能够调,不会报错。

344d23590ce77e0189245be1d8e5ae63.png

871a3d6a5e8ea71de9e9c8f58ff7139a.png

这里提一下,如果这里的四个调用全部都出现了,那么其中的x就不能改变

63159bdc8bb7b0110adef1940fdeeeb4.png

❓但是如果将后面两个const调用的语句屏蔽掉就能够编译成功,这是什么原因呢?

✅我们写的PerfectForward是一个函数模板,在编译运行的过程中会实例化出来四个不同的函数,由于调用传参的过程中有const修饰的形参被实例化,所以实例化后的此函数就会报错。


1.5.2 完美转发

接下来我们将上述的代码进行一点点更改

void Func(int& x)
{
    cout << "lvalue reference" << endl;
}
void Func(int&& x)
{
    cout << "rvalue reference " << endl;
}
void Func(const int& x)
{
    cout << "const lvalue reference " << endl;
}
void Func(const int&& x)
{
    cout << "const rvalue reference " << endl;
}
template<class T>
void PerfectForward(T&& x)
{
    Func(x);
}
void Test7()
{
    PerfectForward(10); // 右值
    int a = 10;
    PerfectForward(a); //左值
    const int b = 8;
    PerfectForward(b); //const 左值
    PerfectForward(std::move(b)); //const右值
}


4e8e7c0cc75c9beeb8ab14bb022668d4.png

运行这个代码我们发现,不管是左值引用还是右值引用,只能调用到左值版本的Func函数,原因在上文中已经说过了:右值引用之后变量本身是左值,所以只能调用到左值版本的Func,那么如何能让左值的调用左值版本,右值的调用右值版本呢?


使用完美转发std::forward,std::forward 完美转发在传参的过程中保留对象原生类型属性

9394eb70563ee5b346e0b47484c5fbcd.png


2. 新的类功能


2.1 默认成员函数

在之前的文章【C++】类和对象中我们讲到类的默认成员函数一共有六个:

  • 构造函数
  • 析构函数
  • 拷贝构造函数
  • 赋值重载函数
  • 取地址重载
  • const取地址重载

其中重要的是前4个,后两个用处不大,但是在C++11中新增了两个默认成员函数:移动构造函数和移动赋值重载函数,这就与我们上文中讲到的对应起来了。

同样的这两个函数也有一些比较难搞的特性:


1. 移动构造函数

编译器自动生成的条件:

  1. 没有自己实现移动构造;
  2. 没有实现析构函数、拷贝构造、拷贝赋值重载中的任意一个

自动生成的移动构造的特性:

  • 对于内置类型,将会逐成员按字节进行拷贝
  • 对于自定义类型,如果这个类型成员有移动构造就调用移动构造,否则就调用拷贝构造


2. 移动赋值重载函数

编译器自动生成的条件

  1. 没有自己实现移动赋值重载
  2. 没有实现析构函数、拷贝构造、拷贝赋值重载中的任意一个

自动生成的移动赋值重载的特性:

  • 对于内置类型,将会逐成员按字节进行拷贝
  • 对于自定义类型,如果这个类型成员有移动赋值就调用移动赋值,否则就调用移动赋值


我们看下面一段代码:

class Person
{
public:
    Person(const char* name = "", int age = 0)
        :_name(name)
        , _age(age)
    {}
private:
    zht::string _name;
    int _age;
};
void Test8()
{
    Person p1;
    Person p2 = p1;
    Person p3 = std::move(p1);
    Person p4;
    p4 = std::move(p2);
}


运行结果如下:

19c02d204f18b224792c964bdde8f5de.png

分析一下这个结果:

首先可以看到的是Person这个类没有实现移动构造,并且没有实现析构函数、拷贝构造、拷贝赋值重载中的任意一个,所以编译器自动生成了移动构造和其他的默认构造函数。


对于第15行内容,由于p1是左值,所以匹配到拷贝构造,Person中默认生成的拷贝构造会调用zht::string中的拷贝构造,因此输出string(const string& s) -- 深拷贝,对于第16行,将p1move之后,变成了右值,因此调用Person自动生成的移动构造,这个移动构造将会调用zht::string中的移动构造,因此输出string(string&& s) -- 移动构造,对于18行,p2move之后变成右值,调用Person自动生成的移动赋值,此函数调用zht::string中的移动赋值,因此输出string& operator=(string&& s) -- 移动赋值。


2.2 类成员变量初始化

C++11允许类定义的时候给定成员变量的初始缺省值,默认生成的构造函数在初始化列表的时候将会使用这些缺省值初始化。

看下面一段代码:

class Date1
{
public:
    int _year;
    int _month;
    int _day;
};
class Date2
{
public:
    int _year = 1970;
    int _month = 1;
    int _day = 1;
};
void Test9()
{
    Date1 d11;
    Date2 d12;
}


4049adcba5a123ef79c128a0f0e107fe.png


在调试过程看到:Date1对应的对象d11中的成员变量都没有被初始化,还是随机值,但是Date2对应的对象d12被默认初始化成了缺省值。


2.3 强制生成默认函数的关键字defaule

在2.1中我们讲到移动构造和移动赋值的默认生成条件比较苛刻。假设我们已经写了拷贝构造,就不会生成移动构造了,但是我们希望移动构造能够被自动生成,就可以使用default关键字显示指定移动构造生成。

class Person
{
public:
    Person(const char* name = "", int age = 0)
        :_name(name)
        , _age(age)
    {}
    Person(const Person& p)
        :_name(p._name)
        ,_age(p._age)
    {}
    //这里使用default显示指定移动构造和移动赋值生成
    Person(Person&& p) = default;
    Person& operator=(Person&& p) = default;
    Person& operator=(const Person& p)
    {
        if(this != &p)
        {
            _name = p._name;
            _age = p._age;
        }
        return *this;
    }
    ~Person()
    {}
private:
    zht::string _name;
    int _age;
};
void Test8()
{
    Person p1;
    Person p2 = p1;
    Person p3 = std::move(p1);
    Person p4;
    p4 = std::move(p2);
}


6190e3690ffb8349af2f13451e8ca71f.png

可以看见此时已经手动写了拷贝构造和拷贝赋值重载,但是还是生成了移动构造和移动赋值重载。


2.4 禁止生成默认函数的关键字delete

如果想要限制某些默认成员函数的生成或者使用,在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) = delete;
private:
    zht::string _name;
    int _age;
};
void Test8()
{
    Person p1;
    Person p2 = p1;
    Person p3 = std::move(p1);
    Person p4;
    p4 = std::move(p2);
}


0a723f52e09eadf3dfc8ba3f1693e834.png

此时调用拷贝构造就会出现报错:尝试引用已删除的函数


2.5继承和多态中的final与override关键字

关于final和override关键字,在之前的博客中已经讲解,这里就不在赘述,有需要的小伙伴可以去看一下

【C++】多态

相关文章
|
30天前
|
Unix 编译器 Linux
C++之模版进阶篇(下)
C++之模版进阶篇(下)
42 0
|
30天前
|
编译器 C++
C++之模版进阶篇(上)
C++之模版进阶篇(上)
14 0
|
30天前
|
编译器 C语言 C++
C++之模版初阶
C++之模版初阶
13 0
|
1月前
|
存储 编译器 C++
【C++模版初阶】——我与C++的不解之缘(七)
【C++模版初阶】——我与C++的不解之缘(七)
|
2月前
|
存储 算法 程序员
C++ 11新特性之可变参数模板
C++ 11新特性之可变参数模板
53 0
|
2月前
|
算法 编译器 程序员
C++ 11新特性之Lambda表达式
C++ 11新特性之Lambda表达式
16 0
|
3月前
|
存储 程序员 C++
【C++小知识】基于范围的for循环(C++11)
【C++小知识】基于范围的for循环(C++11)
|
3月前
|
编译器 C语言 C++
【C++关键字】指针空值nullptr(C++11)
【C++关键字】指针空值nullptr(C++11)
|
3月前
|
存储 编译器 C++
【C++关键字】auto的使用(C++11)
【C++关键字】auto的使用(C++11)
|
4月前
|
安全 编译器 C++
C++一分钟之-泛型Lambda表达式
【7月更文挑战第16天】C++14引入泛型lambda,允许lambda接受任意类型参数,如`[](auto a, auto b) { return a + b; }`。但这也带来类型推导失败、隐式转换和模板参数推导等问题。要避免这些问题,可以明确类型约束、限制隐式转换或显式指定模板参数。示例中,`safeAdd` lambda使用`static_assert`确保只对算术类型执行,展示了一种安全使用泛型lambda的方法。
56 1