程序员自我修养链接加载库 读书笔记

静态链接

  • 预处理 编译 汇编 链接
    • 预处理,展开#
      • #define替换
      • #if #endif替换
      • 处理#include 递归替换
      • 删除注释
      • 添加行号和文件名标识
  • 词法分析,语法分析,语义分析,中间语言生成
  • 链接
    • 地址空间分配
    • 符号决议
    • 重定位

目标文件

  • relocatable executable shared object

  • 格式都一样,布局也一样,代码段数据段

    • .text 代码.data已经初始化的全局变量静态变量 .bss未初始化的全局变量静态变量
      • ->二进制文件也可以强转成relocatable文件 objcopy
    • .plt .got跳转表/全局入口
    • strtab .debug .rodata .hash .line .dynamic
    • .init .fini
    • 自定义段 __attribute__((section("FOO")))

    elf结构

    ELF头
    .text
    .data
    .bss
    其他段
    section header table,段表 readelf查看 elf 头串起段表
    string table
    symbol table…

    特殊符号__executable_start __etext __edata

    符号,name mangleing,extern “C”

    强符号,弱符号,默认强符号 __attribute__((week))

    强引用,弱引用,默认强引用 __attribute__((weakref)) void foo 用来被覆盖

    弱符号典型代表,未初始化的全局变量

静态链接

  • 相似段合并

    • 空间地址分配

      • elf专属地址,其他给个偏移
    • 符号解析重定位

      • 重定位表 object -r a.o
      • 链接需要符号表 readelf -s a.o
      • 链接器不感知类型信息,多个弱符号冲突如何处理? common block ,一种符号提升手段。类似类型提升。
        • (gcc -fno-common, __attribute__((nocommon)))
    • c++相关的链接问题

      • 代码重复消除,模板和虚表造成的膨胀 linkonce段,多余的直接丢弃
      • 函数级别链接,提供接口让函数(或者参数)单独成段
      • 构造函数段和析构函数段
      • ABI问题
      • 静态链接 no-buildin -static –verbose 发生了什么

装载与动态链接

  • 菜谱与炒菜

  • overlay vs paging

    • paging 页映射

      • 创建独立虚拟地址空间

        • 分配一个页,给个页目录结束,不完整的页映射关系,等到页错误再配置
      • 执行文件头,建立虚拟空间与可执行文件的映射,准备照着菜谱炒菜

        • 可执行二进制文件又叫image懂了伐

        • VMA

          image-20200313121136446

      • 将CPU指令集设定成可执行文件入口地址,启动执行

    • 页错误

  • ELF文件链接视图和执行视图

    • 不在乎段占用,段到底什么内容,只注意权限,相同权限合到一起映射 同一个VMA
    • 可执行文件会有程序表头 ProgramHeaderTable来保存映射的段信息(segment)
  • 堆和栈也是VMA cat /proc/pid/maps

  • 可以看到和可执行文件映射vma不同,没有名字 aka AVMA anonymoout virtual memory area
  • 类似堆和栈,vdso 内核交互vma

  • 总结四中VMA类型

      执行 映像文件
    代码 VMA x
    数据VMA
    堆VMA 匿名,无映像,可向上扩展
    x 匿名,无映像,可向下拓展
  • 内核装载ELF的优化

  • 直接为0,bss不映射扔到堆里
  • 段地址对齐以及优化

    • 碎片浪费 ->共享物理页,映射多次
  • 进程栈初始化
  • 内核装载ELF过程简介
    • exec -> sysexec ->do_exec
      • magic number判断开始解释执行 ->binnary_handle -> load_elf_binary
        • elf有效性
        • .interp段存在否,设置动态链接库路径
        • elf文件映射
        • 初始化elf进程环境
        • 系统调用返回地址改成elf可执行文件的入口点 e_entry
        • eip寄存器调到elf程序入口地址,开始执行

动态链接

  • 静态链接磁盘一份内存一份造成的浪费

    • gcc -fPIC -shared -o xx.so xx.c
  • 动态链接程序运行时地址分布

    • 代码段多出来libc ld和动态库

    • 装载时重定位以及地址无关代码PIC

      • 装载时重定位和链接时重定位差不多,没有重复利用代码。引入地址无关PIC可以重复使用,即尽量让地址相关的代码放到数据段

        • 代码段复用,数据段各自复制

          • 模块内部调用,相对地址调用,无需重定位

          • 模块内部数据访问,拿到PC (内部hack)+ 记录的偏移量

            call 484 <__i686.get_pc_thunk.cx>
            add $0x118c, %ecx
            movl $0x1, 0x28(%ecx)
            
          • 模块间数据访问,数据段中建立全局偏移表。间接引用

          • 模块间调用 也是全局偏移表,保存目标函数地址 存在性能问题。elf有优化

            call 484 <__i686.get_pc_thunk.cx>
            add $0x118c, %ecx
            mov 0xfffffffc(%ecx), %eax
            call *(%eax)
            
        • 全局变量怎么处理

          • 可执行文件bss段创建库的全局变量副本 加一条mov来访问
    • 数据段地址无关性

  • 延迟绑定PLT

    • 调用时再绑定(这种理念到处都有啊原来)

      • _dl_runtime_rosolve()
      bar@plt:
      jmp *(bar@GOT)
      push n
      push moduleID
      jump _dl_runtime_resolve
      
    • .got段里拆出来。.got.plt

      • .dynamic地址
      • 本模块id
      • _dl_runtime_resolve地址
  • 动态链接相关结构

    • 引入动态链接器
      • .interp段,专门记录ld目录,字符串
      • .dynamic 导出符号表 .hash 加速查找
      • 重定位 .rel.dyn .rel.plt
    • 动态链接的步骤和实现
      • 动态链接器自举,本身也是动态链接库,需要自举完成状态切换,自举不能访问全局变量调用函数 ,因为没有重定位。.dynamic是入口点
      • 装载共享文件,合并全局符号表
        • 共享库符号冲突?后加入无效
      • 动态链接库的实现
        • 不仅是动态库,还是可执行文件
        • 内核执行不在乎是ET_EXEC还是ET_DYN,就是装载然后转移给ELF入口
          • e_entry, .interp
          • 就elf头不一样,扩展名不一样,其他都一样,window dll和exe也是类似的,rundll32.exe可以吧dll强行按照可执行文件执行
        • _dl_start -> boostrap -> _dl_start_final -> _dl_sysdep_start -> _dl_main _dl_main本身来判断自己是ld还是其他
        • 几点思考
          • 动态链接器本身是动态链接还是静态链接?ldd一下就知道了
          • 动态链接库本身是不是PIC?不是PIC的话,代码段需要重定位,没意义。
          • 动态链接库可以当做可执行文件执行,那么装载地址是?和其他动态库没区别
  • 显示运行时链接

    • 灵活注入动态库。
    • dlopen, dlsym dlerror dlclose
      • dlopen
        1. 查找LD_LIBRARY_PATH
        2. 查找/etc/ld.so.cache
        3. /lib /usr/lib
        4. 返回handle,如果filename为空返回全局符号表
        5. 会执行.init
      • dlsym 根据dlopen返回的handle来查符号

Linux共享库的组织

  • 版本
    • 兼容性 尽量别用c++接口。ABI灾难
    • 命名规则 libname.so.x.y.z
      1. x重大变动,可能不兼容
      2. y增量升级,新增接口
      3. z发布版本号,bugfix,改进等等
    • SO-NAME
      1. 只保留朱版本号的软连
        • 由于历史原因 libc.so.2.6.1 -> libc.so.6 ld.so.2.6.1 ->ld-linux.so
      2. ldconfig
    • 符号版本, 比如glibc的 GLIBC_2.6.1,更新符号来保证依赖
  • 共享库系统路径
    • /lib 系统关键库(动态链接器,c运行时,数学库,bin sbin用到的库)
    • /usr/lib 非系统运行时的关键共享库,静态库,目标文件。不会被用户用到
    • /usr/local/lib 第三方库,python解析器的lib,之类的
  • 共享库的查找过程
    • .dynamic段中DT_NEED列出路径,如果是绝对路径,就会找这个文件,如果是相对路径,就会从/lib /usr/lib /etc/ld.so.conf配置文件指定的目录中查找
      • 每次查/etc/ld.so.conf中的目录必然很慢,ldconfig会cache一份/etc/ld.so.cache
      • 更改/etc/ld.so.conf需要运行ldconfig 重新cache一份
  • 环境变量
    • LD_LIBRARY_PATH 临时更改某个应用程序的共享库查找路径,不影响整体
      • LD_LIBRARY_PATH=/home/user /bin/ls
      • 相同方案,直接启动动态链接器运行程序 /lib/ld-linux.so.2 -library-path /home/user /bin/ls
      • 整体查找顺序 LD_LIBRARY_PATH -> /etc/ld.so.cache -> /usr/lib, /lib
      • 注意不要滥用LD_LIBRARY_PATH,最好不要export
    • LD_PRELOAD 指定覆盖,优先加载,比LD_LIBRARY_PATH优先级还高
      • 同样,有个/etc/ld.so.preload
    • LD_DEBUG 打开动态链接器的调试功能
      • LD_DEBUG=files ls
        • 还支持libs bindings versions reloc symbols statictics all help
  • 共享库的创建和安装
    • gcc -shared -Wl, -soname, my_soname -o libraty_name source_files libraty_files
      • soname 不指定,就没有,ldlconfig就没用
      • 调试先别去掉符号和调试信息(strip),以及-fomit-frame-pointer
      • 查找lib目录,可以临时定义LD_LIBRARY_PATH,也可以-rpath=/home/mylib
      • 符号表,用不到不会导出。如果延迟导入dlopen可能就会反向引用失败,使用-export-dynamic
    • 清除符号信息 strip libfoo.so
      • 生成库不带信息 ld -s/ld -S S debug symbol, s all symbol
    • 共享库安装 ldconfig -n lib_dir
    • 共享库构造与析构
      • ` attribute((constructor))` 在main之前/dlopen返回之前执行
      • __attribute__((destructor)) 在main执行结束/dlclose返回之前执行
      • 必须依赖startfiles stdlib
    • 共享库脚本

内存

  • 程序的内存布局

    • 栈,维护函数调用上下文
    • 堆,动态分配内存区
    • 可执行文件映像
    • 保留区
  • 栈与调用惯例

    • 堆栈帧

      • 函数返回地址和参数

      • 临时变量

      • 保存的上下文 寄存器 ebp espimage-20200324105106459

        • 参数入栈,有遗漏的参数,分配给寄存器
        • 下一条指令的地址入栈,跳转到函数体执行
        push ebp#后面会出栈恢复
        mov ebp, esp
        #sub esp, xxx
        #push xxx
              
        ####结束后,与开头正好相反
        #pop xxx
        mov esp, ebp
        pop ebp
        ret
        
    • 调用惯例

      • 函数参数的传递顺序和方式
      • 栈维护方式
      • 名字修饰策略 name-mangling cdecl
    • 函数返回值传递

      • 寄存器有限,如果返回值太大,寄存器传指针,做复制动作rep move 或者call memcpy
        • 返回值多出来的空间占用,在栈上回预留
      • 流程 栈空间预留,预留地址传给函数-> 函数执行拷贝,把地址传出 ->外层函数把地址指向的对象拷贝 拷贝两次。
        • 返回大对象非常浪费
        • 返回值优化可能会优化掉一次拷贝。
  • 堆与内存管理

    • free list 容易损坏,性能差
    • bitmap 碎片浪费
    • 内存池

运行时

  • main并不是开始

    void _start()
    {
        %ebp = 0;
        int argc = `pop from stack`;
        char** argv = `top of stack`;
        __libc_start_main(main, argc, argv, libc_csu_init, __lib_csu_fini, edx, `top of stack`);
    }
    
  • exit都做了什么

    • 遍历函数链表,执行atexit __cxa_atexit

      movl 4(%esp), %ebx
      movl $__NR_exit, %eax ;call exit
      int $0x80; halt如果exit退出失败,就强制停止。一般走不到这里
      
  • 运行库与IO

  • C/C++运行库

    • 基本功能
      • 启动与退出
      • 标准函数
        • 变长参数,压栈
        • 复杂化printf,所以要指定参数
          • va_list char *
          • va_start 参数末尾
          • va_args获得当前参数的值,调整指针位置
          • va_end,指针清零
      • IO功能封装和实现
      • 堆的封装和实现
      • 语言实现
      • 调试
  • glibc

    • crt1.0 _start crti.o init fini开头 crtn.o init fini结尾
    • crtbegin.o crtend.o c++相关全局构造析构目标文件。属于gcc
  • 运行库和多线程

    • 栈,tls,寄存器私有,其余共有
    • 线程安全
      1. errno等全局变量
      2. strtok等不可重入函数
      3. 内存分配
      4. 异常
      5. IO函数
      6. 信号相关
  • c++全局构造与析构

    • __libc_csu_init -> _init() 调用init段 -> __do_global_ctors_aux
      • 和编译系统相关。 来自crtbegin.ogcc/Crtstuff.c编好。内部会有__CTOR_LIST__ 如何生成?- > 所有的.ctor段拼凑-> crtbegin.o串起来
        • crtend.o负责定义__CTOR_END__指向.ctor末尾
  • IO 初探,通过fread

    • 缓冲buffer
    • 缓冲溢出保护,枷锁 -> 循环读取,缓冲 ->换行符转换 ->读取api

系统调用

  • glibc封装系统调用,可绕过
  • 系统调用原理 image-20200327205248542
  • 特权级与中断
    • 中断向量表( 原来内核都有这玩意儿。。我之前玩stm32也有这东西,以为搞的什么新花样)
    • image-20200327205619506
      • 触发中断陷入内核 ->切换堆栈,保存寄存器信息,每个进程都有自己的内核栈
  • linux新型系统调用,由于int指令性能不佳
    • linux-gate.so.1 aka [vdso] 可以通过maps查看,占用4k,可以导出内部细节就是sysenter等

CRT运行库实现

  • 入口以及exit
  • 实现堆
    • freelist based 堆空间分配算法 malloc free
    • new delete
  • IO与文件操作
  • 格式化字符串

Read More

一月待读

把印象笔记收藏的链接捞出来

https://just-taking-a-ride.com/inside_python_dict/chapter1.html

https://medium.com/@ehudt/redis-hash-table-scan-explained-537cc8bb9f52

https://www.lysator.liu.se/c/pikestyle.html

http://git.kernel.dk/cgit/linux-block/commit/?h=aio-poll&id=5aeaa1ad235c708e31ad930d1ff6ba6fd39bee91

https://blog.conjur.org/special-cases-are-a-code-smell/

https://www.outlyer.com/blog/why-not-to-build-a-time-series-database/

https://99designs.com/blog/engineering/gqlgen-a-graphql-server-generator-for-go/

https://www.haiku-os.org/docs/HIG/index.xml

https://www.scientificamerican.com/article/algorithms-designed-to-fight-poverty-can-actually-make-it-worse/

这个考虑优先翻译

https://arjunsreedharan.org/post/148675821737/memory-allocators-101-write-a-simple-memory#=

https://blog.gopheracademy.com/advent-2018/avoid-gc-overhead-large-heaps/


ref


Read More

怀旧系列 boost fusion

why

最近看了一堆ppt,boost::fusion和boost::proto 出现频率太高,不得不仔细看一遍官网教程总结一下。

boost::fusion 是一套元编程工具,结合编译期与运行时的异构容器和算法组件,在c++0x时代比较有名,现在有了boost::hana这种牛逼的替代品,以后再研究,这个帖主要是总结学习fusion


Heterogeneous computing

boost::fusion::vector std::tuple boost::hana::tuple都是类似的东西,不过std::tuple光秃秃的,只有get,没有相关短发,boost::fusion和boost::hana都是在trick上做了算法加强

std::get<N> 和at_c<N>是一样的

#include <boost/fusion/sequence.hpp>
#include <boost/fusion/include/sequence.hpp>
vector<int, char, std::string> stuff(1, 'x', "howdy");
int i = at_c<0>(stuff);
char ch = at_c<1>(stuff);
std::string s = at_c<2>(stuff);

把Vector打印成XML

struct print_xml
{
    template <typename T>
    void operator()(T const& x) const
    {
        std::cout
            << '<' << typeid(x).name() << '>'
            << x
            << "</" << typeid(x).name() << '>'
            ;
    }
};
for_each(stuff, print_xml());

这个例子有个具体的推导的ppt fusion by example。以后有机会在水一贴

组织

Tuple <- iterator + Sequence + Algorithm <- Support

  • tuple
  • algorithm
    • auxiliary
    • iteration
    • query
    • transformation
  • adapted
    • adt
    • array
    • boost::array
    • boost::tuple
    • mpl
    • std_pair
    • std_tuple
    • struct
  • view
    • filter_view
    • flatten_view
    • iterator_range
    • joint_view
    • nview
    • repetitive_view
    • reverse_view
    • single_view
    • transform_view
    • zip_view
  • container
    • deque
    • list
    • map
    • set
    • vector
    • generation
  • mpl
  • functional
    • adapter
    • generation
    • invocation
  • sequence
    • comparison
    • intrinsic
    • io
  • iterator
  • support

ref

Read More

2017总结

今年就要结束了。回想起今年还算充实。更深入的了解到自己很菜了 新年计划就是读更多的书看更多好看的电影电视剧了。

Read More

十三不靠摘抄

这个是我以前导出到印象笔记的笔记,印象笔记这几年太傻逼了,我准备迁出去,把这种笔记放到豆瓣或者github。

Read More


googletest segmentation fault

自己水平有限,总结下来记录自己菜比挣扎过的时光

最近研究googletest 其实研究很久了,只是琢磨如何用到工作中的模块上。

我将工程代码和自己写的空的测试代码放在一块编译,Makefile中

CPPFLAGS        := -m32 -O1 -g -Wall -fPIC -fpack-struct=1 

然后就遇到崩溃的问题

[==========] Running 1 test from 1 test case.
[----------] Global test environment set-up.
[----------] 1 test from Test
[ RUN      ] Test.Test
Segmentation fault

gdb 跟进去,提示

(gdb) r
Starting program: /mnt/hgfs/share_work/Br_R6.5_r2676/TestCode/RM_TestCode/tes                                                                                 t/run_test.exe
[Thread debugging using libthread_db enabled]
[==========] Running 1 test from 1 test case.
[----------] Global test environment set-up.
[----------] 1 test from Test
[ RUN      ] Test.Test

Program received signal SIGSEGV, Segmentation fault.
testing::internal::scoped_ptr<std::string>::reset (this=0x812d5e8)
    at /usr/include/gtest/internal/gtest-port.h:1172
1172            delete ptr_;
(gdb) q

库肯定不可能有错误。问题到这里根据我的经验就卡住了,实际上在string上。

我用dmesg查看

[52938.831139] run_test.exe[6391]: segfault at f36968 ip 080cf52d sp bff36920 error 4 in run_test.exe[80480                                                   00+ab000]

然后addr2line

addr2line -e run_test.exe 080cf52d -                                                   f
_ZNKSs6_M_repEv
/usr/include/c++/4.4/bits/basic_string.h:281

basic_string 281是这样的

      _Rep*
      _M_rep() const
      { return &((reinterpret_cast<_Rep*> (_M_data()))[-1]); }

然后我就搜到了这个帖子

关于std::string出现在_M_dispose发生SIGABRT错误的问题 - superarhow的专栏 - CSDN博客

肯定是内存对齐导致的,我搜了代码没有#pragma pack

结果发现makefile中有这个选项。。。-fpack-struct=1 就相当于#pragma pack(1)

去掉问题就解决了。

感谢前人们的铺路


Read More

网络延迟-tc工具使用简单说明

在工作中遇到了制造延迟

tc qdisc add dev eth1 root netem delay 600ms

测试部需要的场景比较特殊,只针对核心与组件之间延迟,对于普通设备等不做延迟。还需要保存延迟配置重启不失效

具体的做法是加过滤限制,只针对核心通信的组件进行延迟,组件本身做延迟。

针对不同设备,加到平台配置层里面

核心网上具体的配置 将

 tc qdisc add dev eth1 root handle 1: prio priomap 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
 tc qdisc add dev eth1 parent 1:2 handle 20: netem delay 600ms
 tc filter add dev eth1 parent 1:0 protocol ip u32 match ip dst 192.168.69.23 flowid 1:2
 tc filter add dev eth1 parent 1:0 protocol ip u32 match ip dst 192.168.69.24 flowid 1:2

写在/etc/rc.d/rc.local里面 其中192.168.69.23,24是组件ip eth1是核心对外网卡

组件设备上的具体配置 将

 cd /tcdir && chmod 777 tc
 ./tc qdisc add dev eth1 root netem delay 600ms

写在/opt/local/sbin/osscripts/OSStart 脚本中 其中 tcdir是tc文件所在的目录(不要放在root下) ,eth1是组件对外网卡

tc qdisc del dev eth1 root #删除之前的延迟配置

tc也可以针对端口做限制 以上参考 https://stackoverflow.com/questions/40196730/simulate-network-latency-on-specific-port-using-tc


ref

Read More

More Effective C++笔记

笔记,不值得看。以前记在印象笔记的,搬迁出来做个记录

说实话这个已经过时了。c++最近几年来发展的太快。对应的书已经不值得买了

Read More

(译)advanced metaprogramming in classic c++ 1 templates

第一部分 预备知识 #include


//!个人有疑问的地方以及个人简介会加//? //! //!翻译中可能会有不准的地方,我是结合上下文猜的。不准就不准吧因为水平不到位

1 模板

编程是通过与其中的一台机器交谈来向计算机教授某些东西的过程共同语言。 越接近机器,就越不自然。每种语言都有自己的表现力。 对于任何给定的概念,各种语言的实现有不同的复杂度。在汇编中,我们必须给予非常丰富的描述,(显得不可读)c++的优势就在于在足够接近机器底层的同时又能有优美的表达,C++允许程序员用不同的风格来表达相同的概念,并且更加自然。 接下来深入了解c++模板系统的细节。

下面是一个代码块

double x = sq(3.14);

sq是什么? sq可以是宏

#define sq(x) ((x)*(x))

可以是一个函数

double sq(double x) {return x*x;}

可以是一个模板函数

template

inline T sq(const T& x){ return x*x; }

一个类型(没有实例)//函数对象

class sq
{
    double s_;   
public:
    sq(double x):s_(x*x)
    {}
    operator double() const
    {
        return s_;
    }
};

或者一个全局变量

class sq_t
{
public:
    typedef double value_type;
    value_tupe operator() (double x)
    {
        return x*x;
    }
};
const sq_t sq = sq_t();

不管如何实现,你看到后在脑海中肯定有一个实现,不过脑海中的实现和现实中的实现是不一样的,如果sq是一个类,放在函数模板中可能会有错误的推导结果

template <typename T> void f(T x);

f(cos(3.14)); //实例化f<double>
f(sq(3.14)); //实例化f<sq>?

不仅如此,你还要考虑到各种数据类型被sq平方时在实现上要尽可能的高效,不同的实现有不同的效果

std::vector<double> v;
std::transform(v.begin(),v.end(),v.begin(),sq);

速度瓶颈在sq的实现上。(宏会报错)

###模板元编程TMP的意义在于

  • 所见即所得,不必思考背后的实现
  • 效率最高
  • “自适应”//?自洽?,与其余程序融合,可移植性强

“自适应”意味着移植性强,不拘泥于编译器实现,不受约束,sq数据从一个抽象基类中继承出来可不满足 自洽自适应。 c++模板真正强大的地方在于类型//不变量 考虑两个表达式


double x1 = (-b + sqrt(b\*b - 4\*a\*c))/(2\*a);

double x2 = (-b + sqrt(sq(b) - 4\*a\*c))/(2\*a);

模板的参数计算与类型推导都在编译期间完成,运行时不会有消耗。如果sq实现的好的话第二行要比第一行快且可读性强一些

用sq更优雅

  • 代码可读性强/逻辑自证
  • 没有运行负担
  • 最佳优化

事实上在模板基础上可以轻松加上特化

template <>
inline double sq(const double & x)
{
    //here ,use any special algorithm you have
}
  • 1.1. C++ 模板 - 典型的c++模板,函数模板和类模板//备注:当前c++还支持其他模板,但都可以看做它们的扩展 只要你提供了满足条件的参数,在编译期间模板展开就可以了。 一个函数模板可以推导函数,一个类模板可以展开成类,TMP的要点可以总结为
  • 你可以利用类模板在编译期完成计算。
  • 函数模板可以自动推导他们的参数类型

两种模板都需要把参数列表放在尖括号里。可以是类型或者是整数和指针 //备注 理论上所有整数类型都可以,枚举/bool/typedef/连编译器提供的类型都支持(__int64 MSVC) // 指向全局函数/成员函数的指针也允许,指向变量的指针可能会有限制。在Chapter 11会有讨论 //!当然我能不能翻译到哪里就不好说了

参数也可以有默认值//! 谁不知道啊

模板可以理解成一个元函数,将参数映射成函数或类

比如 sq

template <typename T>
T sq(const T& x);

T -> T() (const T)

同样的,类也是一个映射,比如 T -> basic_string 通过类的模板偏特化来具化元函数,你可以有一个普通的模板和一堆偏特化,它们有没有内容都可以。 定义的时候,类型就是形参,实例化的时候,类型就是实参。 当你向模板提供了实参, 作为元函数//!映射,模板就被实例化,然后生成代码,编译器再将模板生成的代码生成机器码

要明白不同的实参产生不同的实例,即使形参看起来差不多,double和const double实例化的效果可是没有相关性的。

当使用函数模板,编译器会弄明白所有的形参,我们可以理解成形参绑定在模板形参上。

template <typename T>
T sq(const T& x) { return x*x; }
double pi = 3.14;
sq(pi); // the compiler "binds" double to T
double x = sq(3.14); // ok: the compiler deduces that T is double
double x = sq<double>(3.14); // this is legal, but less than ideal

所有的模板实参都是编译期常数

  • 类型形参可以接受任何类型//!只要是类型
  • 非类型形参由最佳转换原则自动推导

一个典型错误例子

template <int N>
class SomeClass{};

int main()
{
    const int A = rand();
    SomeClass<A> s; //error

    static const int B = 2;
    SomeClass<C> s; //fine
}

模板中常量写法的最佳实践是 static const [[integer type]] name = value; //!指的应该不是现代c++,是classic C++

当然,局部变量,static可以省略。不过这个并没有什么坏处,更清晰一些//!不必强求

传递到模板中的实参可以在编译期计算出结果,有效地整数运算都会在编译期得到结果。 //!换句话说,无效的运算都会在编译期被捕捉到而不是放在运行时崩一脸

  • 除以0会导致编译错误
  • 禁止函数调用
  • 生成代码中的非整数/指针类型都是不可移植的。//特指浮点型,可以通过整型除法替代

SomeClass<(27+565) % 4> s1;SomeClass<sizeof(void ) *CHAR_BIT>

除以0的错误的前提是模板整体都是常量

template <int N>
struct tricky
{
    int f(int i =0)
    { return i/N;} //not a constant
};

int main()
{
    tricky<0> t;
    return t.f();
}

waring: potential divide by 0;

改成N/N ,是常数之后就会报错了 实例化N为0或者0/0都会报错

更确切一点,编译期常量包括

  • 整型字面量

  • sizeof,类似sizeof的得到整型结果的操作// alignof

  • 非类型模板形参 在上下文中就是模板wrapper //!原文为”outer” template template class AnotherClass {SomeClass<N> _m; };

  • static 整型常数 template struct NK {

    static const int PRODUCT = N*K;
    

    };

    SomeClass::PRODUCT > s1;

  • 某些宏 LINE SomeClass<__line__> s1; //备注,一般没人这么用,通常用来自动生成枚举/实现断言

模板形参可以依赖其他形参

template<typename T, int (*FUNC)(T)>
class X{};

template<typename T,T VALUE    >
class Y{};

Y<int,7> y1;
Y<double,3> y2;//error  常数3没有这种double类型

类(模板类)也可以有模板成员函数

struct math
{
    template <typename T>
    T sq(T x) const {return x*x;}
};

template <typename T>
struct _math{
    template <typename _T>//备注 T和_T区分避免被外面的给掩了
    static T product(T x, _T y){return x*y;}
};

double A = math().sq(3.14);
double B = _math<double>().product(3.14,5);
  • 1.1.1 Typename

这个关键字用来

  1. 声明模板形参,替代class歧义
  2. 告诉编译器如果不能识别出来,那它就是类型名

举一个编译器不识别的例子

template<typename T>
struct MyClass{
    typedef double Y;
    typedef T Type;
};

template<>
struct MyClass<int>{
    static const int Y = 314;
    typedef int Type;
}; 


int Q = 8;

template <typename T>
void SomeFunc(){
    MyClass<T>::Y * Q;
    //    这行代表一个Q的指向double的指针?还是314乘8?
}

Y是依赖名字,因为它依赖一个未知的参数T 直接或间接的依赖于一个位置的参数的变量都是依赖名字,都应该明确的用typename说明

//!这解决了我的一个疑问,之前遇到但是没有深究,我太菜了。见代码和注释

template <typename X>
class AnoterClass{
    MyClass<X>::Type t1_;//error
    typename MyClass<X>::Type t2_;//ok 
    MyClass<double>::Type t3_; //ok
};

要明白上面这个例子中,第一个必须有typename,第三个不能有typename

template <typename X>
class AnotherClass{
    typename MyClass<X>::Y member1_;//ok 但是X是int不会编译
    typename MyClass<double>::Y member2_;//error
};

当声明了一个没有类型的模板形参时,需要typename引入依赖名字//!来推导出类型

template <typename T,typename T::type N>
struct SomeClass{};

struct S1{
    typedef int type;
};

SomeClass<S1,3> x;

//!接下来这段不好翻译 对于类型T1::T2如果实例化中发现是没有类型的,需要加上typename,等待后面实例化 直接上代码 ​ tmeplate ​ struct B{ ​ static const int N = sizeof(A::X); ​ //应该是sizeof(typename A…) ​ }; 直到实例化,B认为应该调用sizeof在没有类型的参数上,当然啦sizeof也会自己推导出来,所以代码没错,如果X是一个类型,这个代码也是合法的//!后面偏特化 见代码 ​ template ​ struct A{ ​ static const int X = 7; ​ };

template <>
struct A<char>{
    typedef double X;
};

上面的例子没有覆盖一些阴暗的角落,有兴趣请点击这个阴暗的连接

尖括号 angle brackets

即使模板参数有默认参数,你也不能省略尖括号

template<typename T=double> 
class sum{};
sum<> s1;//ok
sum s2;//err

模板参数T可以有不同的含义

  • 表示泛型,比如std::vector std::set 可能要求T有构造比较的语义(conceptual)不影响整体泛型
  • 表示满足固定的条件。这个场景下,仅有部分T实现。比如basic_string<T>

在第二个场景下,你可能想省掉尖括号,两种解决方法,继承或者typedef

template<typename char_t = char>
class basic_string{...};
class my_string : public basic_string<>{
    ...
    // 注意 析构函数不是虚函数
};
typedef basic_string<wchar_t> your_string;

在c++98环境, 复合模板,两个 »连起来可能会被解析成operator » (猜测是parser的贪心解析) c++11解决了这个问题


通用构造函数

赋值构造函数和拷贝构造函数都是泛型的,当类型相同可能匹配不到无法调用

template<typename T>
class something{
    public:
    //S==T 不会调用
    template<typename S>
    something(const something<T>& s){}
    
    //S==T 不会调用
    template<typename S>
    something& operator=(const something<S>& that){
        return *this;
    }
}

something<int> s0;
something<double> s1, s2;
s0=s1;// 调用用户定义的复制构造
s1=s2;// 调用编译器生成的赋值构造

这种用户定义模板成员被称作通用(universal)拷贝构造和通用赋值,这些函数接受something<X> 而不是X

c++标准12.8有描述

  • 模板构造函数永远不会是拷贝构造函数(?), 这种模板不会影响隐式拷贝构造函数生成
  • 模板构造函数与其他构造函数(包括拷贝构造)一起参与重载决议,如果模板构造函数提供更好的匹配那就用模板构造函数来复制

实际上,基类特别泛型的模板成员函数会引入bug,一例

struct base{
  base(){}
  template<typename T>
  base(T x){}
};
struct derived :base{
    derived(){}
    derived(const derived& that):base(that){}
};
derived d1;
derived d2=d1;// stack overflow

隐式拷贝构造函数调用,永远不会调用通用模板构造函数,对于derived,通常来说,编译器为他生成了隐式拷贝构造函数,来调用base的隐式构造函数,但是derived实现了一个拷贝构造函数,转发给了base,调用了通用构造函数,T=derived,在base (T x)中,由于值语义,又调用了T的拷贝构造函数,递归了。


函数类型与函数指针

注意区别

template<double F(int)>
struct A{};
template<double (*F)(int)>
struct B{};

double f(int){return 3.14;}
A<f> t1;
B<f> t2;

通常一个函数会退化成函数指针,这和数组退化成数组指针是一个道理,但是函数类型是不能被构造的(函数唯一)。指针可以。

template<typename T>
struct X{
    T member_;
    X(T value):member_(value){}
};
X<double(int)> tl(f);// err 不能构造
X<double (*)(int)> t2(f); // 指针可以

所以需要functor出场。函数模板参数需要传入引用来避免退化(先加一层)

template<typename T>
X<T> identify_by_val(T x){
    return X<T>(X);
}
template<typename T>
X<T> identify_by_ref(const T& x){
    return X<T>(x);
}
identify_by_val(f); // 退化
identify_by_ref(f); //没问题

对于指针而言,函数还是显式参数的模板函数没有差别

double f(double x){return x+1;}
template <typename T> T g(T x) {return x+1;}
typename double (*pointer_type)(double);
pointer_type f1=f;
pointer_type f2 = g<double>

但是如果这个赋值语境在一个还不确定的模板参数中,就需要template关键字

template <typename X> 
struct outer{
    template <typename T>
    static T g(T x){
        return x+1;
    }
};
template <typename X>
void do_it(){
    pointer_type f1=outer<X>::g<double>; //err
    pointer_type f2=outer<X>::template g<double>;//ok
}

如果要是一个内部类,那就需要typename和template

template <typename X>
struct outer{
    template<typename T>
    struct inner{};
};

template <typename X>
void do_it(){
    typename outer<X>::template inner<double> I;
}

不是模板的基类

如果模板类成员不依赖模板参数,可拆出来放到普通类中

template <typename T>
class MyClass{
    double value_;
    std::string name_;
    std::vector<T> data_;
public:
	std::string getName() const;
};

//改进

class MyBaseClass{
protected:
    ~MyBaseClass(){}
    double value_;
    std::string name_;
public:
    std::string getName() const;
};

template <typename T>
class Myclass :MyBaseclass{
    std::vector<T> data_;
public:
    using MyBaseClass::getName;
};

如果这个模板被实例化很多次的话这也算是个小优化。


模板位置

模板函数类在编译器实例化的期间都必须可见,因此通常的头文件实现分开的做法可能不能直接用,所有实现都放在头文件中,或者改名,hpp

有时候需要前向声明某个特别的实例,前向声明可能通过编译,但是链接还是会有问题,不过有特殊的语法来帮助

template class X<double>;
template double sq<double>(const double&);

可以理解成“导出” c++11 之后有extern 模板

特化和参数推导

前提知识,作用域。命名空间作用域,类空间作用域,函数空间作用域

函数模板和重载,自动推导参数,通常来说,编译器会选择参数最匹配的特化函数,通常一个已经存在的匹配如果可以的话总是会被选中?但是如果存在转化就会有其他场景。

如果函数f比函数g更特化,那就可以把所有调用g的地方都换成调用f,反之则不行。另外,一个非模板函数总是比模板函数更特化一点。

template <typename T> inline 
T sq(const T& x); // 函数模板 1
inline double sq(const double& x);//重载 2
template <> inline
int sq(const int& x);//前面函数模板的特化 3
inline double sq(float x);//重载,可以有不同参数,没问题 4
template <> inline
int sq(const int x);//无效的特化,参数不一致了。需要和前面的函数模板模式一致 5

重载和特化函数的区别就在于函数模板当作一个实体,尽管能特化出各种函数。比如在只有1 2 3的条件下调用sq(y),会在 1 2中选择,如果y是double,就选2,否则就选1 然后1 实例化一个y类型的函数,如果y是int,恰好有个已经特化好的实例3,就选3

考虑下面这个例子

template <typename T>
void f(const T& x){
    std::cout<<"i am f(reference)";
}//1
template <typename T>
void f(const T* x){
    std::cout<<"i am f(pointer)";
}//2

template <typename T> void f(T){}//3
template <typename T> void f(T*){}//4
template <> void f(int*){}//  冲突! 有很多特化路径
template <> void f<int>(int*){}//可以

以上这些特化场景是在命名空间范围内的,考虑一个类空间的例子

class mathematics{
    template <typenmae T> inline 
    T sq(const T&x){}//模板成员函数
    template <> inline int sq(const int&x ){} // err!
};

//解决办法,扔到外面去
template <typename T> inline 
T gsq(const T&x){}
template <> inline 
int gsq(const int& x){}

class mathmatics{
    template<typename T> inline 
    T sq(const T&x){
        return gsq(x);
    }
};

有时候会有不需要推导的函数模板参数,参数只是做个tag dispatch

class crr32{/*...*/};
class adler{/*...*/};
template <typename algorithm_t>
size_t hash_using(const char* x){/*...*/}
size_t j = hash_using<crc32>("this is the string to be hashed");
// 可以加上下面这个,不改变原意
template <typename algotirhm_t, typename string_t>
int hash_using(const string_t& x);
std::string arg("hash me");
int j= hash_using<crc32>(arg);

注意参数推导只针对函数模板,类模板不行。

上面不依靠推导而显式提供模板参数是个坏主意,但有些时候也没法避免,比如

//确实有歧义了
template <typename T>
T max(const T& a, const T&b){/*...*/}
int a=7;
long b=6;
long m1=max(a,b);// err! ambiguous, T==int or long?
long m2=max<long>(a,b);

//参数不需要推导,模板参数做dispatch用
template <typename T>
T get_random(){}
double r=get_random<double>();

//想要一个类似c++ cast方法的函数模板
template <typename X, typename T>
X sabotage_cast(T* p){
    return reinterpret_cast<X>(p+1);
}
std::sring s="don't do this";
double *p =sabotage_cast<double*>(&s);

//想要转换类型
double y = sq<int>(6.28); //把6.28转成int

//模板有默认参数,通常是个tag类,要改变tag
template <typename LessCompare>
void nonstd_sort(..., LessCompare cmp=LessCompare()){};
nonstd_sort<std::less<...> >(...);//传模板参数
nonstd_sort(..., std::less<...>());//传函数参数

一个模板的名字,比如std::vector 和具体实例化的名字是不一样的。但是在类作用域中,他们是一样的

template <typename T>
class something{
public:
	something(){}// 在这层写something<T>是错误的
	something(const something& that);// something& 就是something<T>&
	...
};

如果单独写something, 就代表一个模板. c++中有模板的模板参数, 可以声明模板,模板参数依赖一个模板.

template <template <typename T> class X>
class example{
    X<int> x1_;
    X<double> X2_;
};
typedef example<somthing> some_example; //ok
//注意,这里的class和typename不相等
 template <template <typename T> typename X> //err

类模板可以全特化/偏特化

//1 通常T不是指针
template <typename T>
struct is_a_pointer_type{
    static const int value =1;
};
//2 针对void* 全特化
template<>
struct is_a_pointer_type<void*>
{
	static const int value =2; 	   
};
偏特化所有其他指针类型
template<typename X>
struct is_a_pointer_type<X*>
{
    static const int value =3;
};

int b1= is_a_pointer_type<int*>::value;//匹配3
int b2= is_a_pointer_type<void*>::value;//匹配2
int b3= is_a_pointer_type<float>::value;//匹配1,普通版本

//偏特化可以递归!
template<typename X>
struct is_a_pointer_type<const X>{
    static const int value =is_a_pointer_type<X>::value;
};

至于const 又有一个pointer paradox问题

template <typename T> void f(const T& x){std::cout<< "ref";}
template <typename T> void f(const T* x){std::cout<< "ptr";}
const char* s="text";
f(s);//ptr
f(3.14);//ref
double p=0;
f(&p);//?

也许你会觉得传的指针应该打印ptr,事实上double* 匹配const T*多了个const,这个加const会有副作用,但是匹配const T&正好是值语义,加const 无影响


推导

函数模板可以推导自己的参数,根据函数签名匹配参数类型,这个推导是模式匹配,编译器不会多做其他的计算

template <typename T> struct arg;
template <typename T> void f(arg<T>);
template <typename T> void g(arg<const T>);
arg<int* >a;
f(a);// T=int*
arg<const int>b;
f(b);// T=const int
g(b);// T=int

template <int I> struct argi;
template <int I> arg<I+1> h(argi<I>);
argi<3> c;
h(c);// I=3
// 但是编译器不会帮你计算参数内部的值
template <int I> arg<I> h(argi<I+1>);
argi<3>d;
h(d);//err

另外,如果一个类型包含一个类模板,这个上下文不能被推倒出来

template <typename T> 
void f(typename std::vector<T>::iterator);
std::vector<double> v;
f(v.begin());//err
f<double>(v.begin());//ok

这个错误的原因是无法判断T的类型,理论上T可以是任意类型,T和T::value不相关

template <typename T>
struct A{typedef double type;};

解决方法的弊端上面提到过,尽可能利用推倒而不是手写,下面有几个相关场景的代码片

struct base{
    template<int I, typename X> 
    void foo(X,X){}
};

struct derived :public base{
    void foo(int i){
        foo<314>(i,i);
    }
};

//编译错误
1>error: 'derived::foo': function call missing argument list; use
'&derived::foo' to create a pointer to member
1>error: '<' : no conversion from 'int' to 'void (__cdecl
derived::* )(int)'
1> There are no conversions from integral values to pointer-to-
member values
1>error: '<' : illegal, left operand has type 'void (__cdecl
derived::* )(int)'
1>warning: '>' : unsafe use of type 'bool' in operation
1>warning: '>' : operator has no effect; expected operator with
side-effect

还有一点,如果名字查找有多个结果,显式提供参数会限制重载决议

template <typename T> void f();
template <int N> void f();
f<double>();
f<7>();

但会忽略掉一些重载结果。

template <typename T> void g(T x);
double pi = 3.14;
g<double>(pi);//ok 
template <typename T> void h(T x);//1
void h(double x);//2
h<double>(pi);// 糟糕!调用1

另一例

template <int I> class X{};
template <int I, typename T> void g(X<i>,T x);//1
template <typename T> void g(X<0>, T x);//2 特化X<0>注意,这是g<T> ,不是g<0,T>
double pi = 3.14;
X<0> x;
g<0>(x,pi);//1
g(x,pi);//2

特化

模板特化只在命名空间作用域有效

struct X{
  template<typename T>  
  class Y{};
  template<>
  class Y<double>{};//err, 但是通常编译器不报错。
};

template <>
class X::Y<double>{}; //ok

要注意可见性,推导的前后顺序问题

template <ytpename T> T sq(const T& x){}
struct A{
    A(int i=3){
        int j=sq(i);//已经推导完毕
    }
};
template<>
int sq(const int& x){}//err

再比如

template <typename T> 
struct C{
    C(C<void>){}
};

template <>
struct C<void>{} //err

//解决方法,前置声明
template<typename T> struct D;
template <> 
struct D<void>{}
template <typename T> 
struct D{
    D(D<void>){}
};

也可以偏特化非类型模板参数(int)

template <typename T, int N>
class MyClass{};//1
template <typename T>
class MyClass<T,0>{};//2
template <typename T, int N>
class MyClass<T*,N>{};
Myclass<void*, 0> m; //err 用1 还是2?

//  组合解决
template<typename T>
class MyClass<T*,0>{};

另外,模板参数依赖前提下,也不可以偏特化,除非完全特化

template <typename int_t, int_t N>
class AnotherClass{};
template <typename T>
class AnotherClass<T,0>{}; //err 0依赖T

template<>
class AnotherClass<int,0>{}; //ok, 全特化

一个模板的特化也许和模板参数完全没关系,不必非得相同成员,函数也可以有不同的签名

template<typename T, int N>
struct base_with_array{
    T data_[N];
    void fill(const T&x){
        std::fill_n(data_,N,x);
    }
};

template<typename T>
struct base_with_array<T,0>{
    void file(const T&){}
};

template <typename T, size_t N>
class cached_vector : private base_with_array<T,N>{
//...
public:
    cached_vector(){
        this->fill(T());
    }
};

内部类模板

一个类模板可以使另一个模板的成员,关键点在于,内部类拥有自己的参数,但了解所有的外部类参数

template<typename T>
class outer{
public:
	template<typename X> 
	class inner{
        //可以用T 和X
	};
};

如果T确定(well-defined)类型,就可以用outer<T>::inner<X> 来访问,如果T是模板模板参数,需要加template outer<T>::template inner<X>

内部类通常很难特化。特化应该在outer就列好。列出一些组合场景

template <typename T>
class outer{
    template<typename X>    class inner{}; //inner1
};

template<>
class outer<int>
{
    template <typename X>    class inner{}; //inner2这种全特化前提下,肯定会略过inner1
};

template <>
class outer <int>::inner<float>{}: //inner3, inner2全特化

template <>
template <typename X>
class outer<double>::inner }{};     //inner4, inner1特化,就像inner2

template <>
template <>
class outer<double>:;:inner<char>{}; //inner5, inner4  全特化

template<typename T>
template <>
class outer<T>::inner<float>{}; //err 保持T泛型特化X,这样做的潜台词是无论t是什么inner<X> 都是一致的,从同一个空间的角度就能证伪

int main(){
    outer<double>::inner<void> I1;
    outer<int>::inner<void> I2;
    I1=I2;//err
}

想用一个函数来证明两个inner<X>是相同的也是不现实的(?) 因为无法推导外部的outer<T>

解决办法也有,提升到全局模板。

template <typename X> // typedef //
struct basic_inner{};

template <typename T>
struct outer{
	//typedef basic_inner inner;    
	template <typename X>
	struct inner : public basic_inner<X>{
      inner& operator=(const basic_inner<X>& that){
          static_cast<basic_inner<X>&>(*this)=that;
          return *this;
      }
	};
};
template<>
struct outer<int>{
    //typedef basic_inner inner
    template <typename X>
	struct inner : public basic_inner<X>{
      inner& operator=(const basic_inner<X>& that){
          static_cast<basic_inner<X>&>(*this)=that;
          return *this;
      }
	};
};


然后,需要把basic_inner设计的更泛型支持多种操作

template <typename X, typename T>
struct basic_inner
{
	template <typename T2>
	basic_inner& operator=(const basic_inner<X, T2>&)
	{ /* ... */ }
};
template <typename T>
struct outer
{
	template <typename X>
	struct inner : public basic_inner<X, T>
	{
		template <typename ANOTHER_T>
		inner& operator=(const basic_inner<X, ANOTHER_T>& that)
		{
			static_cast<basic_inner<X, T>&>(*this) = that;
			return *this;
		}
	};
};

template <>
struct outer<int>
{
	template <typename X>
	struct inner : public basic_inner<X, int>
	{
		template <typename ANOTHER_T>
		inner& operator=(const basic_inner<X, ANOTHER_T>& that)
		{
			static_cast<basic_inner<X, int>&>(*this) = that;
			return *this;
		}
	};
};

int main()
{
	outer<double>::inner<void> I1;
	outer<int>::inner<void> I2;
	I1 = I2; // ok: it ends up calling basic_inner::operator=
}

这种实现被叫做SCARY initialization ` N2911 explains that the acronym SCARY “describes assignments and initializations that are Seemingly erroneous (Constrained by conflicting generic parameters), but Actually work with the Right implementation (unconstrained bY the conflict due to minimized dependencies).` 简单说就是共享同一份内部实现。

看参考中援引的论文学习一哈

再考虑内部类中的函数。

如果偏特化出现的比使用晚,就会选取模板,如果使用全特化,直接报错已经实例化了。

struct A
{
	template <typename X, typename Y>
	struct B
	{
		void do_it() {} // do it 1
	};
	
    void f()
	{
		B<int,int> b; //实例化了
		b.do_it();
    }
};

template <typename X>
struct A::B<X, X> 
B<X,X> //   太晚了
{
	void do_it() {} // do_it 2
};
A a;
a.f(); // do it 1
template <>
struct A::B<int, int>
{
	void do_it() {} // compile err
};

风格惯例,约定 style conventions

风格约定

在原有的风格基础上保持一致就可以了,比如满足标准库风格,一个好的风格会省不少事儿,就不用跟到函数内部看这个函数到底实现了啥。

接口功能也是一个注意点,比如是否需要返回错误码/抛异常,接上面的话题,异常风格融合到标准库风格中,不要做多余的事儿。

命名风格也要注意,标准库预留了一些变量,下划线开头,所以不要用下划线开头的变量。包含$符号的(编译器问题),包含双下划线的

对于现代编译器来说,应该没什么,会检测到错误。

注释

对于TMP这种trick技术,需要注释,避免误解。

宏在TMP 中比较邪恶但是又很必要。宏是全局可见的,并且可能会有名字冲突

作者提供了一个方法,前缀 MXT_ 全大写表示全局的,mXT_前缀表示局部的,全部小写的宏用来map标准库,平台

#ifndef MXT_filename_
#define MXT_filename_
#define mXT_MYVALUE 3
const int VALUE = mXT_MYVALUE;
#undef mXT_MYVALUE
#ifdef _WIN32
#define mxt_native_db1_is_finite _finita
#else
#define mxt_native_db1_is_finite isfinite
#endif
#endif

还有一些宏替换关键字trick,extern,namespace,visiable等

#define MXT_NAMESPACE_BEGIN(x) namespace x{
#define MXT_NAMESPACE_END(x) }
#define MXT_NAMESPACE_NULL_BEGIN() namespace x{
#define MXT_NAMESPACE_NULL_END() }

还有BOOST_AUTO这种初始化(有点像汇编是怎么回事)

也可以用宏来生成代码。这是比较常用的场景

#define mXT_C(name, value)			\
static T name()						\
{									\
	static const T name##_ = value;	\
	return name##_;					\
}

template <typename T>
struct constant {
    mXT_C(Pi, acos(T(-1)));
    mXT_C(TwoPi, 2*acos(T(-1)));
    mXT_C(Log2, log(T(2)));
};
#undef mXT_C
double x = xonstant<double>::TwoPi();

//也有一些常用的宏
#define MXT_M_MAX(a, b) ((a)<(b)?(b):(a))
#define MXT_M_MIN(a, b) ((a)<(b)?(a):(b))
#define MXT_M_ABS(a)    ((a)<0?-(a):(a))
#define MXT_M_SQ(a)		((a)*(a))
template <int N> 
struct SomeClass{
    static const int value = MXT_M_SQ(N)/MXT_M_MAX(N,1)
};

c++11 有constexpr能更好的实现上面这段

constexpr int sq(int) {return n*n;}
constexpr int max(int a, int b) {return a<b?b:a;}
template <int N>
struct SomeClass {
  static const int value = sq(N)  /max(N,1);
};

符号`

就是命名风格,作者给的方案,系统级别函数,和标准库风格等同,(c也是这风格,单词还短。算是陋习)

文件名,就都小写,

项目级别类,驼峰

functor和函数风格相同

泛型

提高泛型化的方法就是复用标准库,std::pair, std::tuple

拿std::pair来说,可能p.first和p.second丢失名字信息,如何解决这个问题?几种方案

struct id_value{
    int id;
    double value;
};

//std::pair<int,double>更泛型,但是名字信息丢了

//用宏,保留名字信息
//第一种实现,不建议使用
#define id first 
#define value second
//第二种,稍微强一点
#define id(P) P.first
#define value(P) P,second

//用全局函数,也就是accessor
inline int& id(std::pair<int, double> p){return p.first;}
inline double& value(std::pair<int,double> p){return p.second;}

//evil,使用成员指针
typedef std::pair<int, double> id_value;
int id_value::*ID = &id_value::first;
double id_value::VALUE = &id_value::second;
std::pair<int,double> p;
p.*ID = -5;
p.*VALUE = 3.14;
//make them const
int id_value::* const ID = &id_value::first;


模板参数

作者给的建议是非类型模板参数 Non-Type template paremeter建议全部大写

template <typename T, bool BIGENDIAN=false>
class SomeClass{};

template <typename T>
class SomeClass<T, true>{};

//更干净的声明
template<typename T, bool = false>
class SomeClass;

类型的话通常都是T, 如果有指代信息,会用 A, R 表示参数和返回值(arguments, results)

int foo(double x){return 6+x;}
template <typename R, typename A>
inline R apply(R (*F)(A),A arg)
{
    return F(arg);
}
double x = apply(&foo, 3.14);

其他场景,作者建议直接写小写,后缀_t , 比如int_t, scalar_t

后缀_t通常是c惯用法,typedef,同样,在c++中也有很多类似的用法,(c++更多的是后缀_type, 场景通常是类内部typeder,把模板参数封装一下,作者也是推荐的)

元函数

stateless,无状态,只转发,只做map

template <typename T, int N>
struct F{
    typedef T* pointer_type;
    typedef T& reference_type;
    static const size_t value = sizeof(T)*N;
};

这个元函数做了以下映射

(T,N) -> (pointer_type, reference_type, value)

{type}x{int} ->{type}X{type}X{size_t}

大多数元函数只是做类型映射,或者常量映射

两个例子

//type set -> smaller type set
template <typename T>
struct largest_precision_type;
typename <>
struct largest_precision_type<float>{
    typedef double type;
};
typename <>
struct largest_precision_type<double>{
    typedef double type;
};
typename <>
struct largest_precision_type<int>{
    typedef long type;
};
// const -> const
template <unsigned int N>
struct two_to{
    static const unsigned int value = (1<<N);
};

template <unsigned int N>
struct another_two_to{
  enum{value}= (1<<N)} ;// enum hack
};

unsigned int i = two_to<5>::value;
largest_precision<int>::type j = i+100;
//c++ 11 
largest_precision<decltype(i)>::type j = i+100;

通常,使用enum hack而不是用static const ,不占用空间,而且某些编译器static const有问题。还有一个问题是,static const可能会被取地址,用来做一些evil的事情(复用成普通int,全局变量),enum不会有这种问题。

template <int N>
is_prime{
    enum{value =0};
};
template <>
is_prime<2>{
    enmu{value = 1};
};
template <>
is_prime<3>{
    enmu{value = 1};
};
...

进一步说还有其它问题,比如static const 不一定是编译期计算的。

static const int z = rand();

enum也有问题,因为看起来是int实际上是enum类型,某些场景就需要cast

double data[10];
std::fill_n(data, is_prime<3>::value, 3.14);//perhaps not ok
std::fill_n(data, int(is_prime<3>::value), 3.14);//ok

metafunction helper

一个例子

template <int N>
struct ttnp1_helper{
    static const int value = (1<<N);
};
template <int N>
struct two_to_plus_one{
    static const int value = ttnp1_helper<N>::value+1;
};

//或者直接这么写
template <int N>
struct two_to_olus_one{
private:
    static const int aux =(1<<N);
public:
    static const int value = aux+1;
};

helper 应该不被使用,可以匿名空间或者有个规范,加后缀_helper表示库能用客户端别用?

` 命名空间和using`

命名空间别嵌套太多,否则影响ADL

using可别放在头文件,命名空间混一起就麻烦了

典型模式 classic patterns

size_t ptrdiff_t

通常没有好的选择大整数的办法,那就选这俩,size_t无符号,ptrdiff_t有符号。足够用

size_t可是operator new的参数,也是sizeof的返回值,大小足够用了,ptrdiif_t是算两个指针举例的,也算是够用

进一步,考虑flat c++ memory model,sizeof(size_t)和指针大小是一样的。(无论什么平台)

考虑下面这个类

template <int N>
struct A{
    char data[N];
};

sizeof(A<N>)最起码N,所以size_t不会小于int,同理可证ptrdiff_f

void T::swap(T&)

需要T提供T::swap(T&),性能不能比传统的三次复制差(最次应该相同),理论上是可行的,调用成员的swap就可以了

std::swap/std::container<\T>::swap针对trivial类型已经做了足够的优化,对于用户实现的T,保证T::swap的实现能用上std::swap/std::container<\T>::swap 应该就够用 (std::swap调用的就是成员的swap,各类容器会提供std::swap的特化版本,无缝结合)

效率完全取决于这个T是不是swapable,std::array<T,N> 的swap调用的是swap_range,效率会差一些,但是string实现决定是swapable的,交换会非常快,利用这种类型的swap会有优势

那首先要考虑的问题就是T未知如何swap

template <typename T>
class TheClass{
    T theObj_;
    void swap(TheClass<T> & that){
        std::swap(theObj_, that,theObj_);
    }
};
// 去掉std::限制会有问题
using std::swap;
template <typename T>
class TheClass2{
    T theObj_;
    void swap(TheClass<T> & that){
        swap(theObj_, that,theObj_);// compile err: match one...
    }
};

名称查找的问题,解决办法是加一层调用,引入adl(或者干脆就加上std::好不好,为了省五个字,多打了五行)

using std::swap;
template <typename T>
inline void swap_with_adl(T& a,T& b){
    swap(a,b);
}

template <typename T>
class TheClass3{
    T theObj_;
    void swap(TheClass<T> & that){
        swap_with_adl(theObj_, that,theObj_);
    }
};

也有可能还有swap重载,本质上都是为了找使用std::swap来保证最佳效率。毕竟大部分T::operator=也是用swap实现的

bool T::empty() const, void T::clear()

要求永远是O1,这里有个坑,empty的实现不一定是size()==0,size()也没要求过复杂度,c++98 list的size()就是O(N) 的,后面才改成O1 没什么好说的

clear是一个状态重置,语义上不保证释放资源/内存,所以就有这个swap惯用法

std::vector<int> x(10000,5);
std::vector<int>().swap(x);

X T::get() const, X T::base() const

get 是智能指针设计上的小技巧,T封装了一层X ,get就直接返回了,在智能指针实现上,就是返回指针

base返回值,感觉和get很想但是语义不一样。std::reverse_iterator就用了这个。

X T::property()

property就是个名字,这个std::iostream在用

Action(Value), Action(range)

这个就是没有for-range和std::span的妥协产物, 了解即可

操作符 manipulators

这个在iostream这套邪恶的库中,算是个亮点,但是导致stream本身不是stateless,增加了复杂度

就比如, std::endl实际上是什么东西

class ostream{
public:
    //...
    inline ostream& endl(ostream& os){
        os<<'\n'  ;
        os.flush();
    }
    ostream& operator<<(ostream& (*f)(ostream&)){
        return f(*this)
    }
};

再比如setprecision,实现是什么样的

struct precision_proxy_t{
    int prec;
};
inline ostream& operator<<(ostream&o, precision_proxy_t p){
    o.precision(p.prec);
    return o;
}
precision_proxy_t setprecision(int p){
    precision_proxy_t result = {p};
    return result;
}
cout<<setprecision(12)<<3.14;

一个更成熟的实现是会把proxy存个函数指针,然后setprecision返回proxy<int,fp>然后直接就调用fp了。此处略过。写着实在是闹心。

operators位置

本质上还是拷贝构造的问题

作者建议实现放在类的外面,帮助抓错

假如实现pair

template <typename T1, typename T2>
struct pair{
	T1 first;
	T2 second;
	template <typename S1, typename S2>
	pair(const pair<S1, S2>& that): first(that.first), second(that.second)
	{}
};

如果在内部实现operator== 类型就不能和T1T2重复,假设你实现了个operator== 模板参数和上面相同

template <typename T1, typename T2>
struct pair
{
// ...
	inline bool operator== (const pair<T1,T2>& that) const{
		return (first == that.first) && (second == that.second);
	}
};
pair<int, std::string> P(1,"abcdefghijklmnop");
pair<const int, std::string> Q(1,"qrstuvwxyz");
if (P == Q) { ... }

问题来了!比较的类型不一致,就会调用默认拷贝构造来调用一个满足条件的参数来比较

如果你把实现放在外面,这种场景会直接报错。

template <typename T1, typename T2>
bool operator== (const pair<T1,T2>& x, const pair<T1,T2>& y) {
	return (x.first == y.first) && (x.second == y.second);
}
//正确的实现
template <typename T1, typename T2 >
struct pair {
	// ...
	template <typename S1, typename S2>
	inline bool operator== (const pair<S1, S2>& that) const {
		return (first == that.first) && (second == that.second);
	}
};

无声无息的继承 Secret Inheritance

如果父类组件比较多,自雷继承父类更像是typedef,相当于一种上层的委托构造,或者是模板typedef

template <typename T1, typename T2> 
class A{};

template <typename T>
class B :public A<T,T>
{};

这种写法,最好要保证A是不可见,使用者拿不到的,不然可能就会a* p=new b;这种用法,就得为a加上析构(因为a本身会有很多数据,很重,而不是仅仅作为一个接口)

一个例子

template<typename T>
class B :std::map<T,T>{}; //bad

namespace std{
template <...>
class map :public _Tree<...> //good, _Tree一般没人知道,不会拿过来直接用
}

还有一个例子,提供相同的接口

template <typename T, int CAP=16>
class C;

template <typename T>
class H{
  H&operator ==(const H&)  const ;
};
template <typename T, int CAP>
class C :public H<T>
{};

因为C 是{T}x{int} operator==要抽离出int,利用这个技巧,可以写一个干净的operator==,只要继承H<T>, 由H<T>来实现operator==就好了

Literal Zero

就是预防传值错误,但是又可以传0,一个匹配的trick

class dumy{};
typedef int dummy::*literal_zero_t;
template <typename T>
class match_literal_not_0_err{
    bool operator==(literal_zero_t)const{
        ...
    }
}

因为literal_zero_t构造不出来,只能转成0才能过。

safe bool

由于类可以实现operator bool,可能就会引入歧义。 实现一个safe operator bool是个很有趣的事儿,引用中列了几个文章

stream是怎么实现的

实现了operator void*

stream s;
if(s)
{
    int i=s+2;// 转换成this,不会编译
    free(s);// oops
}

作者介绍了一个类似上面literal_zero的trick,当然和safe bool idiom差不多

struct boolean_type_t{
     int true_;
};
typedef int boolean_type_t::*boolean_type;
#define mxt_boolena_true &boolean_type_t::true_
#define mxt_boolean_false 0

class stream{
  ...
  operator boolean_type() const{
      ...return mxt_bool
  }
};

初始化

初始化可能值是未定义的(POD),也有可能定义了一部分,初始化还有一些坑

T a(); //err, 函数, T (*a)() 
T b;// ok,有默认构造函数
T c(T());//err, 函数 T(*c)(T (*)())
T d ={};//ok ,对于POD
T e= T();// ok 调用拷贝构造

理论上应该有个optional的默认值,作者建议自己实现一个,非常简单(但不是std::optional那种语义,仅仅作为初始化来用)

template <typename T>
struct initialized_value{
    T result;
    initialized_value():result(){
        
    }
};

代码安全,编译器约定,预处理器

由于TMP编程,优雅(瞎写)先行,这就带来了麻烦, 作者举了个unary_function的例子

class unary_f: public std::unary_function<int, float>{
public:
    //...  
};

int main(){
    unary_f u;
    std::unary_function<int, float>* ptr = &u;
    delete ptr;
}

这个例子看的我十分不适,且不说现在基本没什么人知道unary_function, std::bind 都没啥人用,这套binder太硬核了,见识见识std::bind也就足够,现在都是std::function +lambda,况且作者举例的这个写法就是瞎写

剩下的几个例子直接总结就好了,TMP错误实践

  • 非虚析构函数基类问题 上面这个例子
  • 实现operator T()
  • 声明非显式的一个参数的构造函数 T(a)

编译器假定

这些模板使用背后是编译器的大量工作,不是所有的标准都在编译器上实现了的。一个满足标准(standard-comforming)的编译器应该考虑到所有优化场景,这基本上是不可能的,只能说,编译器不可能比代码表现更差,会有优化点。

但是这些场景也没法避免

  • 意外的编译器错误,ICE
  • 运行时错误,访问错误,coredump,panic
  • 大量的编译链接时间
  • 并不令人满意(suboptimal)的运行速度

前两个问题可能是编译器bug,或者用的不对,第三个可能是模板代码引入,第四个问题可能是编译优化效果太差

inline,内联

inline是编译器决定的,即使你代码中标注了inline,定义声明在一起的通常默认inline,成员函数默认inline,如果定义声明不在一起,就不inline

代码中的inline对于编译器来说就是个hint,编译器最终决定是否inline

我们通常假定

  • 如果函数足够简单,会inline,不管代码片长度
template <typename T, int N>
class recursive{
    recursive<T,N-1> r_;
public:
    int size() const {
        return 1+ r_.size();
    }
};
template <typename T>
class recursive<T, 0>
{
public:
    int size() const {
        return 0;
    }
};

上面这段代码片,调用recursive<T,N>::size()会内联,直接返回N

  • 编译器能优化成无状态的,会内联,典型场景,operator(), functor

functor通常会作为容器的成员,还会占用一字节,可以用空基类优化干掉。

template < typename T, typename less_t = std::less<T> >
class set : private less_t
{
	inline bool less(const T& x, const T& y) const	{
		return static_cast<const less_t&>(*this)(x,y);
	}
public:
	set(const less_t& l = less_t())
	: less_t(l)	{}
	
	void insert(const T& x)	{
	// ...
		if (less(x,y)) // invoking less_t::operator() through *this
		{}
	}
};
错误信息

模板编译错误的错误信息很难看懂,作者讲解了点读编译错误日志的技能

  • 看长模板堆栈
  • 看实现细节, 比如std::_Tree std::map
  • 看拓展的typeder ,比如string就是 std::basic_string<char, …>.
  • 类型不全 incompliete types

还有一些编译器小细节

  • 别怪编译器
  • 开编译警告级别, 别忽略警告 比如什么unsigned signed mismatch,很容易打哈哈就过去了
  • 维护一个编译器bug列表
  • 避免不规范的行为,或者说不要写未定义行为的代码
  • 不要害怕语言特性
  • 别人拿你的代码做什么,可能会卵用,预防性接口

预处理器

macro guard

作者还说了一些库中爱用的技巧,跨平台,版本号定义之类的。not fancy

TMP与遍地临时变量相对应的就是帮助类型,auxiliary type


hollow types 空类型

instance_of

在元编程中特别有用的一个工具

template <typename T>
struct instance_of{
    typedef T type;
    instance_of(int =0){}
};
const instance_of<int> I_INT= instance_of<int>();
const instance_of<double> I_DOUBLE = 0;

没明白这有啥特别好用的,感觉主要是提取类型吧,作者没说

至于为什么提供一个参数,主要是因为const变量可能会被警告未使用,所以赋值0避免

selector

主要是用来实现tag dispatch,也可以用std::integral_constant实现

template <bool PARA>
struct selector{};
typedef selector<true> true_type;
typedef selector<false> false_type;
template <typename T, bool B>
void f(const T& x, selector<B>)
{
}

not fancy,标准库里很多。比如iterator 各种分类以及相关的dispatch

static value

实际上还是std::integral_constant的一个实现,没啥值得说的

大小限制

实际上就是sizeof trick

template <typename T>
class larger_than{
    T body_[2];
};

一定满足sizeof(T) < 2*sizeof(T) < =sizeof(larger_than) 考虑padding

根据这个,可以用来做函数匹配, SFINAE,这个后面会讲,只要记住这个char结合sizeof dummy很好用就行了

typedef char no_type;
typedef larger_than<no_type> yes_type;

静态断言 static assertions

其实c++11出了static_assert关键字,下面这些就是个回溯,已经不重要了。

template <typename T>
void myfunc(){
	typedef typename T::type ERROR_T_DOES_NOT_CONTAIN_type;
	const int ASSERT_T_MUST_HAVE_STATIC_CONSTANT_value(T::value);
};

其实也可以理解成一种traits,不存在就报错,编译期拒绝

boolean assertions

这个实现基本上也就是c++11 static_assert的实现,只声明不实现

template <bool Statement>
struct static_assertion{};
template<> 
struct static_assertion<false>;// unimpl

int main(){
	static_assertion<sizeof(int)==3144> ASSERT_LARGE_INT;
}

或者直接用sizeof求值,进一步用宏封装起来

#define MXT_ASSERT(statement) sizeof(static_assertion<statement>)

但是又有了新问题,如果statement中有逗号,按照宏的处理,就会报错,这种场景就得使用两个括号括起来,或者把带逗号的表达式typedef替换掉。比较难看

typedef std::map<int, double> map_type;
MXT_ASSERT( is_well_defined<map_type>::value );
MXT_ASSERT(( is_well_defined< std::map<int, double> >::value ));

assert legal

这也是sizeof的妙用,求值,sizeof求表达式的返回值,所以表达式就得合法

tagging techniques

作者举的例子就是函数重载+空类tag,上面也讲过tag dispatch。没啥说的

一个著名的例子就是迭代器的tag了,针对不同的tag匹配不同的advance函数

此外,作者还举了几个特殊的例子

tag iteration,使用std::integral_constant来做函数匹配迭代,迭代周期应该是/2而不是-1,这样会生成多余的模板。

不过一般也没人把tag dispatch和递归函数叠加着用,太evil了

tag & inheritance, 这个更邪恶, 直接复制书的原话吧

Suppose you are given a simple allocator class, which, given a fixed size, will allocate one block of memory of that length.

You now wrap it up in a larger allocator. Assuming for simplicity that most memory requests have a size equal to a power of two, you can assemble a compound_pool that will contain a fixed_size_allocator for J=1,2,4,8. It will also resort to ::operator new when no suitable J exists (all at compile-time). The syntax for this allocation is 11 : compound_pool<64> A; double* p = A.allocate();

The sketch of the idea is this. compound_pool contains a fixed_size_allocator and derives from compound_pool<N/2>. So, it can directly honor the allocation requests of N bytes and dispatch all other tags to base classes. If the last base, compound_pool<0>, takes the call, no better match exists, so it will call operator new.

More precisely, every class has a pick function that returns either an allocator reference or a pointer. The call tag is static_value<size_t, N>, where N is the size of the requested memory block.

template <size_t SIZE>
struct fixed_size_allocator
{
	void* get_block();
};
template <size_t SIZE>
class compound_pool;

template < >
class compound_pool<0>
{
protected:
	template <size_t N>
	void* pick(static_value<size_t, N>){
		return ::operator new(N);
	}
};

template <size_t SIZE>
class compound_pool : compound_pool<SIZE/2>
{
	fixed_size_allocator<SIZE> p_;
protected:
	using compound_pool<SIZE/2>::pick;
	fixed_size_allocator<SIZE>& pick(static_value<SIZE>){
		return p_;
	}
public:
	template <typename object_t>
	object_t* allocate(){
		typedef static_value<size_t, sizeof(object_t)> selector_t;
		return static_cast<object_t*>(get_pointer(this->pick(selector_t())));
	}
private:
	template <size_t N>
	void* get_pointer(fixed_size_allocator<N>& p){
		return p.get_block();
	}
	void* get_pointer(void* p){
		return p;
	}
};

ref


Read More

^