【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++】多态

相关文章
|
程序员 编译器 C++
【实战指南】C++ lambda表达式使用总结
Lambda表达式是C++11引入的特性,简洁灵活,可作为匿名函数使用,支持捕获变量,提升代码可读性与开发效率。本文详解其基本用法与捕获机制。
418 86
|
编译器 C++ 容器
【c++11】c++11新特性(上)(列表初始化、右值引用和移动语义、类的新默认成员函数、lambda表达式)
C++11为C++带来了革命性变化,引入了列表初始化、右值引用、移动语义、类的新默认成员函数和lambda表达式等特性。列表初始化统一了对象初始化方式,initializer_list简化了容器多元素初始化;右值引用和移动语义优化了资源管理,减少拷贝开销;类新增移动构造和移动赋值函数提升性能;lambda表达式提供匿名函数对象,增强代码简洁性和灵活性。这些特性共同推动了现代C++编程的发展,提升了开发效率与程序性能。
561 12
|
存储 机器学习/深度学习 编译器
【C++终极篇】C++11:编程新纪元的神秘力量揭秘
【C++终极篇】C++11:编程新纪元的神秘力量揭秘
|
算法 编译器 C++
【C++11】lambda表达式
C++11 引入了 Lambda 表达式,这是一种定义匿名函数的方式,极大提升了代码的简洁性和可维护性。本文详细介绍了 Lambda 表达式的语法、捕获机制及应用场景,包括在标准算法、排序和事件回调中的使用,以及高级特性如捕获 `this` 指针和可变 Lambda 表达式。通过这些内容,读者可以全面掌握 Lambda 表达式,提升 C++ 编程技能。
862 3
|
编译器 C++ 开发者
【C++篇】深度解析类与对象(下)
在上一篇博客中,我们学习了C++的基础类与对象概念,包括类的定义、对象的使用和构造函数的作用。在这一篇,我们将深入探讨C++类的一些重要特性,如构造函数的高级用法、类型转换、static成员、友元、内部类、匿名对象,以及对象拷贝优化等。这些内容可以帮助你更好地理解和应用面向对象编程的核心理念,提升代码的健壮性、灵活性和可维护性。
|
编译器 C语言 C++
类和对象的简述(c++篇)
类和对象的简述(c++篇)
|
人工智能 机器人 编译器
c++模板初阶----函数模板与类模板
class 类模板名private://类内成员声明class Apublic:A(T val):a(val){}private:T a;return 0;运行结果:注意:类模板中的成员函数若是放在类外定义时,需要加模板参数列表。return 0;
298 0
|
存储 编译器 程序员
c++的类(附含explicit关键字,友元,内部类)
本文介绍了C++中类的核心概念与用法,涵盖封装、继承、多态三大特性。重点讲解了类的定义(`class`与`struct`)、访问限定符(`private`、`public`、`protected`)、类的作用域及成员函数的声明与定义分离。同时深入探讨了类的大小计算、`this`指针、默认成员函数(构造函数、析构函数、拷贝构造、赋值重载)以及运算符重载等内容。 文章还详细分析了`explicit`关键字的作用、静态成员(变量与函数)、友元(友元函数与友元类)的概念及其使用场景,并简要介绍了内部类的特性。
476 0
|
设计模式 安全 C++
【C++进阶】特殊类设计 && 单例模式
通过对特殊类设计和单例模式的深入探讨,我们可以更好地设计和实现复杂的C++程序。特殊类设计提高了代码的安全性和可维护性,而单例模式则确保类的唯一实例性和全局访问性。理解并掌握这些高级设计技巧,对于提升C++编程水平至关重要。
293 16
|
编译器 C++
类和对象(中 )C++
本文详细讲解了C++中的默认成员函数,包括构造函数、析构函数、拷贝构造函数、赋值运算符重载和取地址运算符重载等内容。重点分析了各函数的特点、使用场景及相互关系,如构造函数的主要任务是初始化对象,而非创建空间;析构函数用于清理资源;拷贝构造与赋值运算符的区别在于前者用于创建新对象,后者用于已存在的对象赋值。同时,文章还探讨了运算符重载的规则及其应用场景,并通过实例加深理解。最后强调,若类中存在资源管理,需显式定义拷贝构造和赋值运算符以避免浅拷贝问题。