STL源码:准备知识(右值引用、forward转发、变参数模板)

QT(5.8版本)标准库源码主要在安装目录下的(VC找inlcude目录)
在这里插入图片描述
扩充内容的源码在
在这里插入图片描述

OOP 和 GP

oop(面向对象编程):Object-Oriented Programming,数据和操作放在一起(同一个类中)
GP(泛型编程):Generic Programming,数据和操作分开

使用GP:
Container和Algorithms团队可各自设计,其间以Iterators沟通即可
Algorithms通过Iterators确定操作范围,并通过Iterators取用Container元素
在这里插入图片描述
举例之:sort

源文件:stl_algo.h
有两个函数,分别是自定义从小到大版本,和允许接收仿函数自定义排序规则版本,两者都调用底层的 __sort函数
在这里插入图片描述
__sort是底层实现,重点在于对introsort_loop的调用,关于它的写法不同版本可能有些许不同,但重点在于它必须是可以随机访问的迭代器,即RandomAccessIterator,否则无法实现 *2(或/2)的操作,这也是为什么list不能使用::sort()的原因
在这里插入图片描述

c++ forward转发

在学习源码之前,需要先了解c++中的forward转发的用法

非常好的一篇:https://www.cnblogs.com/kaleidopink/p/13720773.html

右值引用原理:https://blog.csdn.net/m0_54850825/article/details/124761539

背景

背景:在C++11出现之前,C++传值默认是copy,但copy开销很大
举例:(a = b + c + d)中,c+d是一个临时变量, b+(c+d)又是另一个临时变量
右值:这些临时变量在C++11中被定义为右值(rvalue,read value),右值没有相应的变量名存储它们
左值:与右值相对的是左值(lvalue,localtor value),左值有变量名

场景:

class A {...};
A a;
a.set("temp");//将"temp"复制给a的成员变量

可能的底层步骤:
临时变量"temp"在传参时先被复制一遍
被复制的内容放到a的成员变量中去
回收临时变量

可以改进的地方:临时变量既然是要回收的,那么可以直接使成员变量接管临时变量,就可以避免中间的复制过程
move和forward就是为此而产生的

左值引用和右值引用

左值引用

1. 原理

关于引用:&b=a实际上等价于int* const b=&a,而编译器会把&b编译为:&(*b)
它的实现是:将a的地址放到寄存器中,然后再将寄存器中的地址传给引用变量b

常量之所以可以左值引用,是因为它被放在静态存储区,是有地址的。而像是10这样的临时变量是放在寄存器中的,无法取址

2. 使用和注意事项

左值引用要求右值必须能够取地址,如果不能取地址,则必须为常引用。因为左值引用本质上是将地址赋给左值

int &b = a + 1;//错误,左值引用必须能够取地址

a+1不能被认为一个在内存中存在地址的变量,如果要这样用,需要加上const修饰。

同样的函数返回值不能使用左值引用,例如 int &a = func();这种情况要么修改为常引用或者不使用引用,但常引用会导致引用不可修改。

普通引用只能引用左值,不能引用右值,const引用既可引用左值,也可引用右值

当使用&b=a; 对b的操作相当于对(*b)的操作,即对a的操作

右值引用

1. 原理

int&& ref = 3;

临时变量(编译时未分配内存或使用寄存器存放的值)引用关联到右值时,右值被存储到特定位置
它的汇编指令和const &b = 3是一样的,只是后者不允许修改

用临时变量将3存起来,然后将地址送入寄存器;寄存器再将地址传给ref

变量和临时变量的值实际是按同一方式处理的,也就是说,临时变量根本上来说就是一个没有名字的变量而已,从汇编的角度来说,都是指针

C++引入右值引用,是为了延长临时变量的生命周期,避免昂贵的copy开销。相当于用指针直接接管这一块内存,而不是重新申请内存再复制

2. 使用和注意事项

右值引用可以处理临时变量的情况,即无法寻址的情况

类型 &&引用名 = 右值表达式;//定义格式

右值引用可以延长临时变量的生存周期,避免了无谓的内存复制操作。

C++11中右值引用:只能引用右值,一般情况不能直接引用左值

单纯的右值引用没有意义,它常与move和forward一起使用

引用叠加

int x = 1;
int&& r1 = x;//实际上这一步就会报错了
auto&& r2 = r1;

r2最终是一个左值引用
所有的右值引用叠加到右值引用上仍然是一个右值引用;
所有其他类型之间的叠加将会使得变成一个左值引用

move移动语义

参考:https://blog.csdn.net/u013015629/article/details/116525759

move是将对象的状态或者所有权从一个对象转移到另一个对象,只是转义,没有内存拷贝。

int d = 1;
int &&r = d;

上述操作是不被允许的,如果需要用右值引用接收左值,可以采用move函数(当不确定接收的是左值还是右值时可以这样使用)

int &&r = std::move(d);

而且move几乎没有代价,只是转移了资源的控制权。尽量可以用。

  • 如果没有提供移动构造函数,只提供了拷贝构造函数,std::move()会失效但是不会发生错误,因为编译器找不到移动构造函数就去寻找拷贝构造函数,也这是拷贝构造函数的参数是const T&常量左值引用的原因
  • c++11中的所有容器都实现了move语义,move只是转移了资源的控制权,本质上是将左值强制转化为右值使用,以用于移动拷贝或赋值,避免对含有资源的对象发生无谓的拷贝。move对于拥有如内存、文件句柄等资源的成员的对象有效,如果是一些基本类型,如int和char[10]数组等,如果使用move,仍会发生拷贝(因为没有对应的移动构造函数),所以说move对含有资源的对象说更有意义。

forward完美转发

move会将左值强制变成右值使用,而forward会保持原变量的属性
若对一个对象做move操作,就不要声明为 const. 因为对const对象的move请求会执行到copy操作上

void func(int&& i) {
    cout << "rvalue: " << i << endl;
}
void func(int& i) {
    cout << "lvalue: " << i << endl;
}


int main() {
    int val = 2;
    int &m1 = val;
    //在不使用move或者forward时
    func(m1);//lvalue
    func(3);//rvalue
    //使用move会将左值变成右值
    func(std::move(m1));//rvalue
    func(std::move(3));//rvalue
    //使用forward会保持原有特性
    func(std::forward<int&>(m1));//lvalue
    func(std::forward<int>(3));//rvalue

    return 0;
}

上述代码中可以看到move和forward的不同
如果仅是这样的用法,体现不出move或者forward的意义,它们常常是用在需要中间函数转发的情况,如下面代码所示。
如果不使用move和forward,经过中间函数rpass的转发,右值将变成左值

void func(int&& i) {
    cout << "rvalue: " << i << endl;
}
void func(int& i) {
    cout << "lvalue: " << i << endl;
}
void lpass(int& i){
    func(i);//lvalue
    func(std::move(i));//rvalue
    func(std::forward<int &>(i));//lvalue
}
void rpass(int&& i){
    func(i);//lvalue
    func(std::move(i));//rvalue
    func(std::forward<int &&>(i));//rvalue
}
int main() {
    int val = 2;
    lpass(val);
    rpass(3);
    return 0;
}

为了解决左值变右值的问题,C++用了move将左值右值都转为右值。
进一步的,为了使得左值始终为左值,右值始终为右值,C++推出了完美转发(Perfect Forwarding),即在函数模板中,完全按照模板的参数的类型(保持参数的左值、右值特征)进行参数的传递。

变参数模板

参考:https://zhuanlan.zhihu.com/p/394184676

C++11引入的新特性,参数个数和类型都可能发生变化的模板。变参数模板必须使用到模板形参包

模板形参包

模板形参包是可以接受0个或者n个模板实参的模板形参,至少有一个模板形参包的模板就可以称作变参数模板

模板形参包主要出现在函数模板和类模板中,主要有三种,即:非类型模板形参包、类型模板形参包、模板模板形参包。

非类型模板形参包

相较于class和typename而言的,实际上是固定类型形参包,如:

template<int ... data> xxxxxx;

这个类型只能为整型、指针和引用

  • 示例:函数模板
//class type实际上用不到,但是因为template<>不能为空,所以要加上
template<class type>
void printAmt(int &iSumAge){
    return;
}

template<class type, int age0, int ... age>
void printAmt(int &iSumAge){
    iSumAge += age0;
    //这里sizeof ... (age)是计算形参包里的形参个数,返回类型是std::size_t,后续同理
    if ( (sizeof ... (age)) > 0 ){
        //这里的age...其实就是语法中的一种包展开
        printAmt<type, age...>(iSumAge);
    }
}

int main(){
    int sumAge = 0;
    printAmt<int,1,2,3,4,5,7,6,8>(sumAge);
    cout << "the sum of age is " << sumAge << endl;
    return 0;
}

(1)开头的空模板必须要写,因为递归访问形参包(每次取形参包中的一个),最后一次必然是一个空参数调用
(2)sizeof … (age)返回类型是size_t,代表形参个数
(3)传递给非类型模板形参包的实参不是类型,而是实际的值

类型模板形参包

和前面的类似,只是这里的形参包的类型可以不再固定

typename|class ... Args
  • 示例
void xprintf(){
    cout << endl;
}

template<typename T, typename... Targs>
void xprintf(T value, Targs... Fargs){
	//这里调用的时候没有显式指定模板,是因为函数模板可以根据函数参数自动推导
    if ( (sizeof ...(Fargs)) > 0 ) xprintf(Fargs...);
    else xprintf();
}

int main(){
    xprintf("小明个人信息:", "小明", "男", 35, "程序员", 169.5);
    return 0;
}

模板模板形参包

template < 形参列表 > class ... Args(可选)
  • 示例1:模板作为参数

场景:要使用vector,但暂时不知道vector是什么类型。于是可以将vector和类型作为参数,新建一个模板,这个新的模板有一个模板形参(如传来的vector),还有一个类型形参,如下面的代码:

#include <typeinfo>
#include <cxxabi.h>
#include <iostream>
#include <vector>

//将gcc编译出来的类型翻译为真实的类型
const char* GetRealType(const char* p_szSingleType) {
    const char* szRealType = abi::__cxa_demangle(p_szSingleType, nullptr, nullptr, nullptr);
    return szRealType;
}

//这里的func是一个模板模板形参
template<template<typename, typename> class func, typename tp, typename alloc = std::allocator<tp> >
struct temp_traits {
    using type = func<tp, alloc>;
    type tt;//根据模板类型定义一个成员变量
};

int main() {
    temp_traits<std::vector, int> _traits;
    //获取结构体字段tt的类型
    const std::type_info &info = typeid(_traits.tt);
    std::cout << GetRealType(info.name()) << std::endl;
    return 0;
}
//输出:std::vector<int, std::allocator<int> >
//还可以传入其他模板和类型
temp_traits<std::list, double> _traits;
//输出:std::__cxx11::list<double, std::allocator<double> >
  • 示例二:模板作为参数,及可变模板参数
#include <typeinfo>
#include <cxxabi.h>
#include <iostream>
#include <vector>
#include <list>
#include <deque>

//将gcc编译出来的类型翻译为真实的类型
const char* GetRealType(const char* p_szSingleTyp {
    const char* szRealType = abi::__cxa_demangle(p_szSingleType, nullptr, nullptr, nullptr);
    return szRealType;
}

void xprintf() {
    std::cout << "调用空函数" << std::endl;
}

template<typename tp, typename alloc, template<typename, typename> class T, template<typename, typename> class ... Targs >
void xprintf(T<tp, alloc> value, Targs<tp, alloc>... Fargs) {
    std::cout << "容器类型:" << GetRealType(typeid(value).name()) << std::endl;
    std::cout << "容器数据:" << std::endl;
    auto it = value.begin();
    for(; it != value.end(); ++it) std::cout << *it << ',';
    std::cout << std::endl;
    //这里调用的时候没有显式指定模板,是因为函数模板可以根据函数参数自动推导
    if ( (sizeof ...(Fargs)) > 0 ) xprintf(Fargs...);
	else xprintf();
}

int main(){
    std::vector<int> vt;
    std::deque<int> dq;
    std::list<int> ls;
    for(int i =0 ; i < 10 ; ++i) {
        vt.push_back(i);
        dq.push_back(i);
        ls.push_back(i);
    }
    xprintf(vt, dq, ls);
    return 0;
}

形参包的使用方法

(1)就像上面的例子那样,递归的方法一个个取出形参包里的形参,最后使用一个默认函数来处理形参包为空的情况

(2)直接展开形参包,然后将所有参数传递给另一个函数等

template<class type, int age0, int ... age>
void printAmt(int &iSumAge){
    iSumAge += age0;
    (void)std::initializer_list<int>{
        (std::cout << age <<",", 0)...
    };
}

int main(){
    int sumAge = 0;
    printAmt<int,1,2,3,4,5,7,6,8>(sumAge);
    std::cout << "the sum of age is " << sumAge << std::endl;
    return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值