代码阅读笔记

记录一些遇到过的大大小小的C++坑,或者是C++比较优秀的惯用技巧、设计模式~

我将平时阅读的一些项目代码的笔记总结于此。

俗话说,读代码比写代码要难数倍,尤其是读别人的代码。的确如此,不过,不管从什么角度统计,我们花在读代码上的时间都远多于写代码的时间,而如何将代码写的清晰易懂、便于维护,又是一个不简单的问题。在代码的“品味”上,怎样做出优雅的设计,是一件需要积累的工作,没有十足的经验恐怕是难以达成。虽说经验可以通过自己写代码来增加,但我一直认为,阅读好其他人的代码,品味其中的设计思路,吸收其中的设计优点,才会如虎添翼,迅速增加自己的“代码感”。

此外,阅读代码本身也是一门学问。如何高效地阅读数量众多,成千上万行起步的代码,并将其中的设计架构提取出来,也不是一件容易的事。

GLM

GLM是一个Header-only的3D数学库,实现了C++上的GLSL类型与函数,里面的类型定义大量用到了模版。下面描述一下项目从上(用户接口)到下(底层实现)的结构。

  1. 上层的用户可见API均放在了GLM/根目录下,头文件以*.hpp作为扩展名。用户可见的头文件可分为3种:

    • 一种是综合性导入的头文件,里面仅有其他头文件的导入,这种文件主要是为了方便用户导入相关API。比如GLM将项目分为了4个模块(核心功能、稳定扩展、推荐扩展、实验性扩展),对应的就有两个综合性导入头文件:一个用于导入核心API的glm.hpp,一个用于导入其余的扩展API的ext.hpp,方便用户导入。
    • 另一些比较具体的类型导入头文件,这种头文件主要是解决同种类型的头文件较多的情况,比如vec3.hpp里面就导入了各种类型的vector3头文件,这些头文件里面主要定义了类型模版的别名,这样使用时就可以不需要填写很长的模版参数了(同时也定义了与GLSL规范相同的类型名称)。
    • 还有一种是包含了声明的头文件(主要是函数声明),这些函数均为模版函数(由于参数类型本身带模版)。声明的头文件中并没有实现,在声明头文件的底部引入了detail/里面实际的实现。

    以上所有的用户可见API均在命名空间glm下。

  2. 核心功能的实现在detail/文件夹下。这个文件下的文件扩展名主要就2种:*.hpp为声明,*.inl为具体实现。一个例外是glm.cpp,该文件中显示实例化了各个类的模版,主要是为了能使该库以静态库或动态库的方式使用。以_*.hpp开头的这些文件是库内部使用一次或多次的头文件,主要实现了一些通用的功能或宏相关的检测处理等:

    • _features.hpp里主要放了有很多编译器功能检测宏,如果发现了编译器支持某些额外特性,就定义一个GLM_CXX??_???的宏,表示可以使用该特性。(不过我没有看到有哪个文件使用了这个文件?)
    • _fixes.hpp里清除了某些编译环境可能预先定义的宏,防止其产生干扰,如maxmin等。
    • _swizzle.hpp_swizzle_func.hpp定义了与swizzle相关的类与宏。
    • _vertorize.hpp实现向量化,就是将同一个函数作用到一个向量上,可以是一元函数或二元函数。

    上面的前两个文件就是为了增加可移植性。

    接下来一个比较重要的文件是setup.hpp,基本上每个文件都会直接或间接地引用到该文件。这个文件中定义了GLM的项目信息宏(版本号、名称字符串),会根据CMake中定义的一些编译选项宏进一步地定义一些宏/类型等,如是否开启编译信息输出、是否开启static_assert、是否使用constexpr、是否使用forceinline等等。此外,该文件内还含有编译器支持的C++特性的检测,如果编译时支持某较新C++的特性,就会定义一个GLM_HAS_???,后序就可以以此使用不同的代码,以增强可移植性。这些根据编译选项定义下来的宏要么在后序作为条件编译用在了#ifdef这样的地方,要么作为关键字展开以实现不同的效果(如GLM_INLINE可以展开为普通的inline__forceinline__attribute__((__always_inline__))),也可以根据平台定义统一的类型(如表示长度的length_tsize_t还是int)。

  3. 之前说了函数的声明在根目录下,而函数的定义在detail/文件夹下,且均以func_*.inl作为文件名。在每个*.inl文件中,可以之前的用户可见函数声明相对应的函数实现。这些实现如果涉及到的只是简单的模版参数,那么在此就直接实现了相关代码;如果涉及到较复杂的实现(例如实现与向量长度有关),那么这段实现中并不会做实际工作,只是首先进行模版参数检测static_assert,然后转发到一个struct的特定静态方法call去。相应的struct定义在命名空间glm::detail中,用户一般不可见。通过对向量长度的偏特化,就可以针对不同长度的向量实现特定的方法,而不支持的长度最终会落到一个空的struct中,导致编译错误。struct中的特定实现可能会用到API本身,这个时候就会从上级根目录中导入其头文件。有一些用到了STL库中已有的函数(比如max),其就在实现中直接通过using std::max进行导入。对于STL函数作用在向量的每个元素上时,用的就是之前说的_vertorize.hpp中的向量化,向量化本身的实现与向量长度有关,也是放在偏特化的struct中。

  4. 向量/矩阵相关的类声明在detail/type_*.hpp中,类定义在detail/type_*.inl中,类定义也有与函数定义类似的转发机制(主要为了处理SIMD相关),转发的目标均在glm::detail命名空间下,用户不可见。

  5. SIMD的转发方式:由于SIMD要求对象是对齐的,如果检测到是对齐的类型,就可以采用SIMD。因此GLM在之前转发到静态方法call时还额外多提供了一个模版参数,表示是否对齐,如果为true就会转发到SIMD的实现。这个是否对齐的判断是用type traits实现的,利用glm::detail::is_aligned完成。在qualifier.hpp中定义了描述精度、对齐的qualifier,根据这些qualifier表示的含义就可以写出对应的判断对齐的type traits。为了实现对齐的要求,这里也定义了一个叫storage的struct,这个struct中会根据align的要求定义不同大小的结构体,之后在具体的类中,将这些结构体与数据成员定义在一个union中即可实现对齐。此外对齐了的storage结构体中可能直接利用SIMD类型进行表示,这样长度就肯定符号SIMD的要求。SIMD的实现函数部分在detail/func_???_simd.inl文件中,类部分就在detail/type_???_simd.inl中,如果检测到支持SIMD且开启的编译选项,SIMD实现文件就会被加入到普通实现文件的底部。这些实现里面可能会用到一些共用的作用于SIMD类型的函数,其定义在simd/*.h中。这里面的函数同时也面向想要使用SIMD编写算法的高级用户。simd/platform.h文件中进行了编译器与平台检测,得知是否开启SSE/AVX/NEON等,再以此导入对应的intrinsic头文件,并对原生的SIMD类型做了typedef,保证不同平台的SIMD类型一致。

文章作者: dhbloo
文章链接: https://dhbloo.github.io/2020/01/14/Proj-Reading-Notes/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 dhb's Blog