一些零碎知识(持续更新)
本文最后更新于:15 天前
python -u是啥
1 |
|
stdout是标准输入,stderr是标准错误,一般来说,标准错误没有缓存区,会直接输出到屏幕上;而stdout会有缓存区,即当出现换行符或者到达一定长度才会输出到屏幕,而增加**-u这个短指令即是使得stdout变为unbuffered**,使得我们的代码顺序即为输出顺序
在python3.6.8的解释器下执行,感觉好像默认了unbuffered😕
pty何物是也,为啥一些指令都有它
关于tty(终端,是一种字符型设备,名字源于teletypewriter,电传打字机):
pty:pseudo terminal伪终端
串口并口
串口(串行接口):一次传输一位数据,较为稳定,可进行长距离通信(串行口COM1,COM2)
并口(并行接口):一次传输八位数据,但受长度限制,因为长度越长会增加干扰,数据容易出错
详情可见硬件基础:硬件通信常见的串口介绍
什么是异构
异构计算(Heterogeneous Computing)是一种特殊的并行分布式计算系统。它能够经济有效地实现高计算能力,可扩展性强,能够非常高效地利用计算资源。与之相对的概念就是同构计算(Homogeneous Computing),也就是为大家熟知的多核理念。为了突破计算算力受制于功耗的瓶颈,多核CPU技术得到越来越多的应用。强大的CPU采用越来越多的CPU内核这就是传统同构计算系统。很快人们就发现在AI人工智能和自动驾驶爆炸式增长的计算需求下,传统同构计算系统已经无法满足要求,GPU、DSP、FPGA和ASIC由于特定需求下高效性越来越多的被应用。而异构计算技术应运而生,像一个大厨将CPU、GPU、DSP、FPGA和ASIC(Application Specific Integrated Circuit,应用专用集成电路,主流有TPU、NPU、VPU、BPU芯片等)这些优良食材制成一道融合各方口味特点的佳肴[^1]。
-
计算场景异构(通俗意义上的异构计算)
CPU+GPU/DPU
DPU:是对网卡和存储等数据的高效处理
何为计算场景的异构呢,就是本来应该是CPU处理的场景,现在解耦出来由专门的芯片进行处理。 -
计算性能的异构
相比于传统的X86芯片的同构多核,ARM在芯片架构上进行创新,采用了异构多核的芯片架构,芯片里面的核心分了大小核 -
CPU架构的异构
应该就是采用X86,ARM,RISC-V,Alpha,LoongArch等不同的CPU架构
进制单位转换
左侧代表的是十进制前缀,右侧表示的是二进制前缀
一般是制造商采用十进制,对于我们来说存储一般指的是二进制的,而在说到带宽一类的速率时,指的是十进制的
./local目录
.local相当于跟全局的python环境进行了一层的隔离,在这里我们可以安装自己需要的库(在本地范围内),而无需提权
类似的安装方式大致如下:
pip install target=~/.local/lib/python3.8/site-packages <package-name>
参考自:https://blog.csdn.net/m0_67402026/article/details/125241407
当然,如果知道有多个解释器,也可以安装到对应的解释器的位置,如:
D:\path\to\python.exe -m pip install --user <package-name>
参考自:https://blog.csdn.net/ThsPool/article/details/132809683
参考:https://geek-docs.com/python/python-ask-answer/603_python_what_is_the_purpose_of_homelocal.html
linux软硬链接
关于linux软链接的创建删除更新,可以这么理解,Linux的软链接相当于快捷方式 ,硬链接好像会共享原来的inode
以下是参考文章:http://www.mobiletrain.org/about/BBS/151003.html
site-packages和distpages区别:
https://blog.csdn.net/weixin_40614261/article/details/90023807
vimrc配置
注意,要查看vimrc在哪可以在一个利用vim打开的文件内通过:version查看,往下滑动可以看到vimrc文件一般配置在:
/etc/vimrc
~/.vimrc
module
module全称:module-environment是一个用于管理环境变量的工具
常用命令
module avail
查看所有的模块module load/add 需要加载的模块
加载需要加载的模块module unload/rm 需要卸载的模块
卸载需要卸载的模块module list
列出已加载的模块module show 模块
查看指定的模块信息
iterable,iterator,generator
关于python中iterable,iterator,generator:https://blog.csdn.net/weixin_44966641/article/details/131501576
不同操作系统的多线程
Windows用spawn,Unix用fork,主要区别见下文:
__getattr__ __getattribute_
descriptor(描述器)之__get__ __set__ __delete__
装饰器
常用装饰器
这里介绍三个常用的装饰器语法糖,需注意函数指的是非类方法,方法指的是类内的方法,分为实例方法,类方法,静态方法
- @property
- @classmethod
- @staticmethod
类属性和实例属性
一般属性和私有属性
参考文章:
常量表达式constexpr
c++的lambda表达式
c++11引入的,主要的写作构造如下:
1 |
|
capture list
,捕获列表,捕获我们作用域中的变量,有值捕获(=),引用捕获(&),混合捕获:- 如[a,b]表示值捕获a,b变量,以const方式;
- 如[&a]表示引用捕获a变量,可修改值;
- 如[=]/[&]表示值/引用捕获作用域的所有变量
parameter list
,参数列表,传参嘛specifiers
,限定符,少用,可忽视exception
,异常说明符,少用,可忽视-> type
,返回值function body
,函数体
使用如下:
1 |
|
FMA和MAC
MAC(Multiply-Accumulation):乘积累加运算
FMA(fused-multiply-add):融合乘加运算
它俩实现的都是这么一个操作,只用一条指令就可以完成(否则要用两条嘛),当然有相搭配的硬件(如乘数累加器)
不同之处在于MAC要规约2次,FMA规约1次即可(更精准,更快速,但也有一些问题)
参考文件:
ldd
这是一条神奇的指令,可以查看可执行文件所需要的动态库(.dll
,.so
)
ldd file

神奇的环境变量名
LD_LIBRARY_PATH
在终端通过echo $LD_LIBRARY_PATH
可以看到一些路径,这些路径是用于指定共享库/动态链接库的搜索路径.因此如果在运行可执行文件时报有.so
或.dll
库找不到的错误,可以通过ldd指令查看是否有库没有被找到
如果想添加一些别的动态库搜索路径,可以通过在~/.bashrc
文件中导出环境变量,并用source ~/.bashrc
刷新一下bashrc文件,即可实现
导出环境变量可以加在bashrc文件末尾,如:
export LD_LIBRARY_PATH=/lib:$LD_LIBRARY_PATH
其中:是路径的分隔符
参考文件:
GDB小工具使用方法
要使用GDB来进行代码调试以完成错误定位,在进行编译的时候要加上-g
的选项
生成可执行文件后,通过gdb filename
的方式进行gdb调试程序
首先通过l
(list缩写)来查看当前程序的代码
一般终端无输入即按回车是重复上一条指令
通过break funcName
来设置断点,也可以通过break lineNum
行数来设置断点。可以通过info break
来查看断点设置情况
执行r
(run缩写)执行程序,程序会执行到端点处暂停,然后通过n
(next缩写)单步越过执行/s
(step缩写)单步步入执行,可以通过c
(continue缩写)来直接执行到下一个断点处,又或者通过u lineNum
(until缩写)来移动到执行的行数
通过q
(quit退出调试程序)
若是在执行到某一个函数内部,排查好了问题,想回到原函数,可以通过finish
完成函数剩下的指令返回,也可以通过return
立刻返回,不执行后面的指令
**如何调试内联汇编?**这是gdb对于算子开发的一个最重要的作用,因为别的错误我们好排查,而一旦牵扯到内联汇编,可能涉及到内存,寄存器的问题:
我们可以通过layout asm
显示汇编代码的窗口,我们只需要打断点到指定的内联汇编开头,然后开启这个窗口,通过si
(step instruction),单步执行指令,然后可以通过p
(print)或display(更推荐)来指定一些想要观察的变量或寄存器(寄存器需要display $v0
加个$
),通过display的话每次执行一条指令都会打印我们跟踪的变量和寄存器的结果,这对于调试内联汇编代码非常有用,可以帮助我们观察一些寄存器和变量的实时变化情况
参考文件:
c++之const的位置
const默认作用于其左边的东西,否则作用于其右边的东西
const applies to the thing left of it. If there is nothing on the left then it applies to the thing right of it.
例子:
-
const int *
是一个指针指向一个常量整型 a pointer to a constant integer -
int* const
是一个常量指针指向一个整型 a constant pointer to an integer -
int const * const
是一个常量指针指向一个常量整型 a contanst pointer to a constant integer -
...
可以通过从右往左读的方式来理解,然后一般写的时候也建议按照eastern const style(把const放东边/右边)来写
注:看的时候脑子冒出来左结合右结合,搜了一下,结合性是为了处理具有相同优先级的运算符时,确定他们在表达式中结合的顺序
左结合即是指在一个表达式中有多个具有相同优先级的运算符,它们在表达式中从左到右依次结合,如:a + b + c
-> (a + b) + c
右结合即是指具有相同优先级的多个运算符在表达式中从右到左依次结合,如:a = b = c
-> a = ( b = c)
参考文件:
参考文件:
C++之namespace
- 全局范围内定义,通过
namespace ns_name{...}
这样定义,不可以局部范围内定义; - namespace可嵌套;
- namespace目的:避免命名冲突;
- 我们要使用某一命名空间内的变量/函数时:
- 用::(作用域限定符)来获得,如std::cin;
- 命名空间全部展开不推荐
using namespace std;
; - 命名空间部分展开
using std::endl;
其中作用域限定符有:全局范围的,类范围的,也有namespace范围的,使用如下:
- global scope:
::var
- class scope:
class::var
- namespace scope:
namespace::var
通过访问左侧的scope来知道想要访问的var是哪里的
参考文件:
C++之左值,纯右值,将亡值
- 左值(left-hand-side value)简称lvalue
- 纯右值(pure right-hand-side value)简称prvalue
- 将亡值(expiring value)简称xvalue
三者的由来与函数返回值相关:
- 直接存在寄存器里返回
- 直接操作用于接收返回值的变量(函数返回值转换为出参,由函数内部直接操作外部的栈空间)
- 使用匿名空间接收函数返回值,这是一个临时的内存空间,用完即析构
prvalue,lvalue,xvalue对应上述三种情况.
这三种值的出现与C是C的extension有关,C的结构体乃至类增加了构造函数和析构函数,并将之与变量的生命周期进行绑定有关
详细情况参看参考文件中腾讯技术工程对这一块的讲解
参考文件:
c++平凡类型与非平凡类型
参考文件:
C++11新特性
参考文件:
C++智能指针
参考文件:
ELF文件格式了解
ELF(Excutable Linkable Format)是linux下可执行文件的存储格式,是COFF(Comman File Format)的一种变体
目标文件,动态库,静态库以及可执行文件都用ELF文件格式来进行存储
一般二进制文件的存储格式中都有文件头,是通过读文件头来确认如何解析文件(个人感觉文件头存储了许多的元数据)
补Elf64_Ehdr和Elf64_Shdr,前者是ELF文件头,里面有比如段表(节头表Section Header Table)的信息,比如有多少个段,段大小,段偏移;后者则是记录每个段表的元信息
参考文件:
decltype和auto
两者都用于自动类型推导,都是在编译期完成的
auto定义变量必须提供初值表达式,形如:auto v = expr
;
decltype则和表达式结合在一起,如:decltype(expr) v;
同时它可以保留变量的引用属性和cv修饰词(const,volatile)信息
对于decltype使用时,可以根据expr有无带括号分类:
-
expr无括号:
- 若expr是单变量标识符,类的数据成员,函数名称,数组名称,则推导出来的类型和expr的类型是一致的;
- 若expr是一条表达式,则视表达式运算后的结果而定:
- 右值,推导类型和运算结果类型一致;
- 左值,推导结果是个引用
-
expr带括号:
(expr)
表示的是执行表达式,并根据返回结果推导类型
1 |
|
参考文件:
name mangling的解释工具
反汇编出的汇编代码,它会有一坨怪怪的东西,比如_Z3addPiPb
之类的,这个东西又大概知道它应该是个函数名,这玩意可以通过c++filt
或是cu++filt
来给它还原,比如:

神奇的宏定义
在看别人代码的时候,不难会发现,有些人的宏定义会出现#
,主要用两个用途:
#
单个井号的使用,使得值变成字串,如#define val2str(val) (#val)
##
两个井号的使用,使得两个值进行拼接,如#define nameJoin(x,y) x##y
可以通过gcc -E ...
使得文件经过预处理,看看宏替换后的效果~
参考文件:
C++ TMP
TMP即templace metaprogramming模板元编程,这里小小的记录一下,更多的细节需要在实践中不断认知
元编程(metaprogramming)是编写元程序(metaprogram)的一种编程技巧
元程序的输入是其他程序乃至自己(metaprogram is a program about a program)
元编程两种类型:
- 一种是对domain language翻译成host language,逻辑代码与元程序是分离的
- 一种是用同一种语言将逻辑代码和元程序写在一起,元程序通过编译得到逻辑代码,并与别的逻辑代码混合,自己manipulate自己,逻辑代码与元程序是混合的,元程序看起来自己在编写自己
CTMP是后者,元程序的代码和c逻辑代码写一起,元程序的逻辑在编译期执行,c++逻辑代码在运行期执行
可声明的模板类型
-
类模板(class template)
1
2template<typename T>
class clsTemplate; -
函数模板(function template)
1
2template<typename T>
void funcTemplate(T) -
变量模板(variable template)
1
2template<typename T>
T varTemplate; -
别名模板(alias template)
1
2template<typename T>
using aliasTemplate = T; -
concept
前三种可以有定义,后两种不需定义(不产生运行时的实体)
模板的产生是为了泛型编程(generic programming),因此类型是被参数化(即类型可做参数)了的。模板由关键字template
表明,然后<>
内放置的是模板的参数
类模板,函数模板是模板而非类或函数,它们是对类和函数的描述,模板是对代码的描述
模板之形参(形参-parameter,实参-argument):
-
Non-type Template Parameters,接受一个确定类型(与模型形参的那个确定类型匹配)的常量作为实参,之所以需要常量,是因为模板是在编译期展开的,这个时候只有常量,没有变量
1
2
3
4
5
6
7
8
9
10template<int n>
void funcPrint(){
cout << n << endl;
}
int main(){
const int b = 3;
funcPrint<b>(); // output 3
return 0;
} -
Type Template Parameters,用**
typename
这个关键字来声明**T是一个类型名称1
2
3
4
5
6
7
8
9
10
11
12template<typename T>
void whichType(){
T val;
cout << typeid(val).name() << endl; // 头文件#include<typeinfo>
}
int main(){
whichType<int>(); // output i
whichType<float>(); // output f
whichType<uint32_t>(); // output j
return 0;
} -
Template Template Parameters,只接受类模板和类的别名模板作为实参,且实参模板参数列表要和形参模板的参数列表match
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24template<typename T>
class clsTemp{
public:
T val;
void display(){
cout << "From clsTemp,datatype:" << typeid(val).name() << endl;
}
};
template<template<typename T> typename tmplClass> // template<typename T>指明了所接受的类模板的形参列表.本类接受的类模板形参经由typename声明为tmplClass这么一个类型
class clsTempTempPara{
public:
tmplClass<int> tmp;
void print(){
cout << "From clsTempTempPara" << endl;
tmp.display();
}
};
int main(){
clsTempTempPara<clsTemp> classTempTempPara; // 类模板作为实参,clsTemp这个类模板的参数列表只有一位,且是type template parameter
classTempTempPara.print();
return 0;
}
模板形参之可变长形参列表
模板的形参也可以是可变长形参列表的,这个列表呢称作template parameter pack,这个可变长的pack要给它放最后(定长前面,变长后面),它可以接受:
-
nontype的constant
1
2
3template<int...args>
class packtmpl1{};
packtmpl1<1,2,3> pinst; -
类型
1
2
3template<typename...args>
class packtmpl2{};
packtmpl2<int,float,string> pinst2; -
类模板
1
2
3
4
template<template<typename T>typename...args>
class packtmpl3{};
packtmpl3<clsTemp> pinst3;
模板实例化
模板的实例化也分显式和隐式(按需),实际上我们一般常用的都是隐式,比如vector<int> vec;
,从这里可以看出,所谓的模板实例化的显式隐式不是以你是否隐式传参来区分的
-
隐式/按需实例化,需要具体的对象再实例化
-
显式实例化,先不创建对象,但是先实例化模板
1
2
3template<typename T> void func(T t){cout << t << endl;}
// 显式实例化
template void func(int);
包含模型
模板编程的包含模型(inclusion model):将模板定义放在头文件,源文件包含这个头文件
模板实例化所产生的symbol是weak symbol,不然会有重定义的问题,说是不同的翻译单元,传入的实参相同,则会实例化多次,产生的类/函数/变量都是相同的;同一个翻译单元则会复用而不是实例化多次,只会实例化一次
模板特化
模板特化(Template Specialization)指的是对模板的形参进行部分替换或全部替换(替换是指具体化,即无泛型特征),并重新定义这种替换了的模板
-
全特化(full specialization),指对原模版的形参全部替换 函数模板只有全特化
1
2
3
4
5
6
7
8
9
10
11
12template<typename T,typename U>
void funcTmpl(T t,U u){
cout << "primary template" << t << " " << u <<endl;
}
template<>
void funcTmpl(int t,float u){
cout << "full specialization" << t << " " << u << endl;
};
int t = 1;
float u = 2.0;
funcTmpl(t,u); // output: full specialization1 2
funcTmpl(t,"123"); // output: primary template1 123 -
偏特化(partial specialization),指对原模版的形参部分替换
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15template<typename T,typename U>
struct clsTmpl{
void display(){cout << "primary template " << endl;}
};
template<typename U>
struct clsTmpl<int,U>{
void display(){cout << "partial specialization " << endl;}
};
template<>
struct clsTmpl<int,float>{
void display(){cout << "full specialization " << endl;}
};
clsTmpl<string,int>ctm1; ctm1.display(); // primary template
clsTmpl<int,int>ctm2; ctm2.display(); // partial specialization
clsTmpl<int,float>ctm3; ctm3.display(); // full specialization -
主模板(primary template),无特化的原始模板
从上面的输出结果可以看出,在实例化模板的时候,编译器会挑最匹配的实现来进行替换,如果找不到才用primary template进行替换
注意区分模板特化和显示实例化,如:
1 |
|
Metafuncion Convention
编译期只有常量和类型,我们管他们统一称为Metadata,其中常量是non-type metadata,而类型是type metadata.对于一个程序,除了数据,还有逻辑,逻辑部分则称为Metafunction,是在编译期会被调用的"函数"
这类"函数"会返回type
,即某一种特定的类型,也会返回value
,这个value
按照上面metadata的分类来说,即是常量.它们会通过别名模板或者是变量模板加以后缀_v
或是_t
用以定义或声明
而一个metafunction如果使用了另一个metafuncion的东西,可以通过继承来实现拥有其所依赖类的全部内容,相较于通过定义using type = xxx<xx>::type
一类的方式更加简洁,同时采用struct关键字可以少写公有继承和类模板内的共两个public
根据参考文件说是只返回type
(这里的返回其实跟函数返回值不一样,可以通过如xxx<T>::type
的方式获取到这个类型)作为唯一的返回值,而已经通过value
表示的常量则通过类模板封装,metafuncion返回这个类模板实例,但感觉把继承用上,其实就直接xxx::value
和xxx::type
去获取这俩了,不用通过xxx::type::value
去获取这个value
根据参考文件(三)中编写程序时遇到了待决名,即语法分析阶段无法获知它是啥,例如remove_reference<T>::type
依赖于T,这是语义分析才会实例化之,才可以对type进行名字查找,但在语法分析要判定语句是否合法,要通过typename显式指定它是一个类型或者通过template显式指定这是一个成员模板
在对类模板实例化时,如何匹配特化的模板?会通过确认实参->代入特化->反向推导对应特化的形参来确认是否匹配,如果失败则会依次fallback至主模板
关于参考文件三的实现:cpp_grammar_tmp
其中关于TMP处理数组,个人理解如int[4][5][6]
进行匹配会倒过来匹配:int[6][5][4]
,先从[4]
开始匹配,此时T
是int[6][5]
模板实参推导
模板实参推导分为函数模板实参推导和类模板实参推导(c++17引入,通过构造函数进行模板实参推导),模板实参无需全部显式传入,可通过函数调用的实参或是构造函数的实参来进行推导,模板实参都能被推导且推导结果无冲突(即不存在T=int又T=float这种情况),则推导成功
SFINAE
SFINAE(Substitution Failure Is Not An Error)即替换失败不是一个错误,说句人话就是实例化失败不会报错,不会产生error
SFINAE这个东西的作用很大,比如可以让我们基于逻辑控制重载集/可以在c11/c14模拟c++17的if constexpr等,但需要先说说模板实例化流程,因为SFINAE作用于其中
-
函数模板的实例化流程
名字查找(找重载)->函数模板实参推导(推导失败的不会报错而是在重载集中删去)->重载决议(偏序规则排序和SFINAE于重载集删去非良构的)->有特化选特化,没特化选主模板->替换生成代码
-
类模板的实例化流程
名字查找(找类模板,多了就re-identification了)->根据主模板的构造函数进行实参推导(出错直接error)->根据主模板和特化选择最匹配的(估计这里冒出来了特化集,这里匹配特化就是metafunction那里的内容),通过将类模板的特化转为函数模板,使用函数模板的偏序规则排序,以及SFINAE对非良构的特化移出特化集->有特选最特,最特挑不出报错,无特选主模板->替换生成代码
对于类模板而言,特化并不会带来新的名字;
对于函数模板而言,重载则是一个新的名字,特化(对函数就是全特化)也不会带来新的名字
这里的新的名字就是重定义指的那个名字
同时需要注意,实例化中并不一定都要通过推导得到模板实参,这个模板实参可以是默认值,可以是显式传入,也可以是推导得来的
上面我们提及了一个偏序规则,同时也提及了类模板的特化转成函数模板的形式采用函数模板的偏序规则进行排序,这个排序的目的是选出最匹配的特化/重载
那么类模板特化如何转换成函数模板呢?
1 |
|
根据上述示例代码可以看出,转换的关键就是将模板特化的形参转换为函数的形参
而后便使用偏序规则对特化进行优先级比较
偏序规则可以这么描述:fa<U>(A)
,其中U表示模板实参,A是实参列表中的类型,它是用U来表示的,即A=op(U)
,将A代入fb
的函数形参列表P,据此推导出T,即用U表示T
当fa<U>(A) => fb<T>(P) && fb<U>(A) ≠>fa<T>(P)
时,说明fa
比fb
偏序更高.而不存在上述关系的二者无法推出谁比谁高,即认为相等
最后来说一下这一部分的重点SFINAE:
替换失败(Subtitution Failure)是指:在进行替换时,模板的立即上下文(immediate context,指声明处)中的类型或表达式非良构(ill-formed,指语法错误),这"不是一个错误"(INAE),只会在重载集/特化集中移除它,不会报错!
关于参考文件五的实现:cpp_grammar_tmp
以下对一些SFINAE实现的模板进行描述:
enable_if<bool,T>
类模板,根据bool的true/false触发SFINAE(非良构),实现基于逻辑控制重载集,可用其"类型"(enable_if_t<bool,T>
)用于如函数的返回值,这样如果enable_if<false,T>
就会非良构从重载集删去,因不会继承type_identity<T>
;而传入的为true
,则会以T
作为返回值void_t<Args...>
别名模板,可以基于此实现如python的hasattr功能,实现为has_member_type
这个类模板,通过template<class T,class U=void> has_member_type : false_type{};
来实现一个主模板,特化模板的形参的void
通过void_t
传入,会根据SFINAE进行判断declval<T>()
函数模板,可以实现编译期的fake variable,作用于不求值运算符内,这个函数模板的实现是只声明不定义,template<T> add_rvalue_reference_t<T> declval();
通过decltype(declval<T>())
获取伪变量的类型add_lvalue_reference<T>
类模板通过一些函数模板实现继承的选择,所谓继承的选择是通过SFINAE实现的,因为如void
是没有引用或右值引用类型的,所以要通过SFINAE匹配到对应的函数模板,如果是void
返回void
,是int
返回int&
,这个返回的是type_identity<T>
或者type_identity<T&>
一类的,而对于int而言这两种返回值都可以被匹配到,都在重载集里,这时候就要用偏序规则挑,那就要从函数的形参列表做文章,如func(...)
或者func(int)
后者显然偏序更高,当传入是int类型的单个参数.所以偏序规则和SFINAE两者要一起结合着用is_copy_assignable<T,U=void>
类模板,本质上跟利用void_t
实现的has_member_type
类模板一样的实现方式,都是应试尽试的想法,先通过declval搞俩假变量,然后赋值,然后通过decltype取类型,通过别名模板换个名字,而后利用void_t
进行替换测试,如果无错则说明替换成功,支持拷贝赋值运算符,否则选到主模板tuple
- 通过template parameter pack定义一个构造函数,作为递归边界;而另一个特化则把这个pack拆成
T
和另一个Args...
,继承于类模板实参为Args...
的类,在当前情况下,T所对应的值,存储于T value_;
这么个成员变量,显然对于不同的Args...
,拆成某一情况下的T
和Args...
对应的value_
是不同的,是属于该类的成员变量.从可变长参数拆到没参数匹配到递归边界 - 原生tuple中的
auto t = std::tuple('1',2.2f,std::string("three")); auto value = get<idx>(t);
显然传入的idx
是类似于前期解构extent
这么个获知某一个维度的数组的N的类模板,那么idx也可以通过类似的方式传入,而括号中的t
通过模板实参推导得到,我们知道传入的一定是tuple,那么可以推导得到的Args...
就会被作为模板形参,由此我们知道了get的大致写法,同时由于我们要获取某一个idx的value_
,本质上是一个upcast的过程(找父类),因此可以通过static_cast<>()
进行转换,与之而来的是转换得到的类型应该是什么? - 显然转换的类型是某一个父类,即
tuple<...>
,这与输入的idx有关,递归至idx为0,则可以取此时的一整个tuple,这里的递归是tuple_element<N,tuple<T,Args...>>:tuple_element<N-1,tuple<Args...>>
,等到N(即是上面的idx)变为0,则对此特化取using __tuple_type = tuple<T,Args...>
,由此上述的upcast的问题便可以通过此metafunction获得,而获得的返回值是此时对应的T,则在这个tuple_element<0,tuple<T,Args...>> : type_identity<T>
得到,而原生的tuple是可以修改值的,因此upcast包括get的返回值应该都是含有引用
- 通过template parameter pack定义一个构造函数,作为递归边界;而另一个特化则把这个pack拆成
不求值(Unevaluated)运算符有sizeof
,decltype
,typeid
(这玩意不是RTTI的东西吗,怎么跑编译期来了),noexcept
,这些运算符的操作数在编译期后就消失了,不求值,只访问操作数的编译期属性.对应的有不求值表达式,不求值上下文
concept和requires
注意此部分没有写测试代码
我们前面对传入的模板实参的筛选,目的是挑选对应的函数或类,以使用特定的具体实现,是通过模板实例化中的特化集和重载集,利用偏序规则和SFINAE去做筛选,有一种反着写代码的感觉,且报错非常冗长.而c++20引入的concept则解决了这个问题,且语法简单清晰.
concept是对于模板实参的一系列constraints的集合,对它的检查先于模板实例化(所以报错少).它的声明如下:
1 |
|
通过约束表达式来声明具体的concept,其中约束表达式支持短路求值,跟之前模板实例化中的逻辑运算表达式不同.具体使用如下:
1 |
|
requires
关键字引入requires从句,放置于函数模板的声明中:
- 可以接在函数模板形参列表
>
的后面; - 也可以放在函数形参列表和函数体之间
funcName() here {}
requires从句必须是一个常量表达式,可以是:
-
concept表达式
1
2template<typename T> requires Numeric<T>
void func(T){} -
true/false
-
约束表达式
1
2template<typename T>
void func(T) requires std::is_integral_v<T> || std::is_floating_point_v<T> {} -
requires表达式
这个表达式声明如下:
1
requires(parameter){...}
它本质是一个bool类型的纯右值,也是描述约束,良构为true,否则为false.可以用于声明concept,也可以用于requires从句
约束出现的顺序决定了编译器检查的顺序,逻辑相同,但约束的位置不同,不会被认为是重定义
泛型编程并非完全泛型,而是约束下的泛型编程,这里的约束即是concept和requires的体现
参考文件:
C++特征萃取
详见type_traits
库,也可以看模板元编程中的部分手动实现
参考文件:
常量折叠
在传统编译器中,常量折叠指的就是编译期算好的常量,它们是放在常量表中的,然后用到的时候直接取值,如下:
1 |
|
g++ -S
编译看到汇编文件中b的值就是$120
而常量折叠这个次也出现在了AI编译器中,指的是计算图优化中对一些输入均为常量值的op提前计算,以消除冗余的计算,下面是知乎ZOMI酱的图(编译器相关的代码我不懂,但是理解理解概念总是需要的)

__attribute__()
为函数,变量,类型提供优化和检查的编译属性,详见参考文件中的内容,可以去到gnu的网站看一些详情和例子,见得比较多的是声明一些类型的最小对齐大小,如:
1 |
|
也可以用来标识一个函数过期了,如
1 |
|
参考文件:
虚表指针
在函数前有个virtual
关键字的就是虚函数,有虚函数的类的实例们就有一个大家统一用的虚函数表,有个虚表指针会指向这个虚表,用以实现多态,通过运行时指向具体子类指针/引用的基类指针/引用来调用对应虚表的函数实现,从而实现多态.
virtual void func() = 0;
纯虚函数,拥有它的类是抽象类,无法实例化,但是可以声明之用以指向实现了纯虚函数的派生类
虚表指针是在类内成员变量的最前面:
1 |
|
根据上面的测试,既然可以得到虚表指针,也即可以通过虚表指针调用对应的内容:
1 |
|
上述是使用虚表指针进行内容调用的一个例子,需要注意虚表指针是二级指针,同时运用多态的时候,析构函数也要是虚函数(基类的析构函数要是虚函数),不然你delete的时候就调用的是基类指针的析构函数,会造成内存泄漏,而给析构函数加个virtual,那delete的时候通过虚表找到对应的析构函数,即可把子类给析构了再析构父类
虚表指针是在内存分配->初始化列表->构造函数内部的内存分配和初始化列表之间完成初始化的
参考文件:
四种cast使用场景
四种cast:static_cast<newType>(expr)
,dynamic_cast<newType>(expr)
,const_cast<newType>(expr)
,reinterpret_cast<newType>(expr)
- 涉及到底层指针的转换采用
reinterpret_cast
- 涉及到cv属性变化的采用
const_cast
- 在单元测试中涉及到downcast的对象指针或对象引用则用(RTTI(runtime type identification)影响性能)
- 平常则用
static_cast
参考文件:
=default / =delete
c++11 =default
可以在类中让编译器生成默认构造函数,拷贝构造函数,移动构造函数,以及对应的赋值运算符,反之可以通过=delete
禁用
函数调用约定
__stdcall
和__cdecl
是两个函数调用约定的关键字,都是从右到左入栈,前者是被调函数清理栈,后者是调用函数清理栈
几个继承
公有继承于派生类就是基类的成员访问权限保持原样;
protected和private则是把基类成员的访问权限全变成protected或是private
非公有继承会影响到后续派生类对象对基类方法的调用,可以通过using来重新定义一下访问权限
参考文章:
友元
友元实际上是一种单向的关系,在将某个函数/类声明为类A的友元函数/类的时候,类A开放了protected,private修饰的成员变量的权限给它,同时这个友元函数/类只需声明在类A(不是类A的函数啥的,只是声明),可以定义在外头,有利于工具类的使用,也有利于泛型编程,也避免了如Java一样的繁复的get,set函数
参考文章:
CRTP
CRTP,curiously recurring template pattern,奇异递归模板模式,静态多态实现方式,其实就是基类是个类模板,模板形参传入的是派生类,基类内部的函数通过static_cast<derived*>(this)->xxx()
的形式实现downcast,实现编译期的多态
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!