记录一些遇到过的大大小小的C++坑,或者是C++比较优秀的惯用技巧、设计模式~
我将平时阅读的一些项目代码的笔记总结于此。
俗话说,读代码比写代码要难数倍,尤其是读别人的代码。的确如此,不过,不管从什么角度统计,我们花在读代码上的时间都远多于写代码的时间,而如何将代码写的清晰易懂、便于维护,又是一个不简单的问题。在代码的“品味”上,怎样做出优雅的设计,是一件需要积累的工作,没有十足的经验恐怕是难以达成。虽说经验可以通过自己写代码来增加,但我一直认为,阅读好其他人的代码,品味其中的设计思路,吸收其中的设计优点,才会如虎添翼,迅速增加自己的“代码感”。
此外,阅读代码本身也是一门学问。如何高效地阅读数量众多,成千上万行起步的代码,并将其中的设计架构提取出来,也不是一件容易的事。
GLM
GLM是一个Header-only的3D数学库,实现了C++上的GLSL类型与函数,里面的类型定义大量用到了模版。下面描述一下项目从上(用户接口)到下(底层实现)的结构。
-
上层的用户可见API均放在了
GLM/
根目录下,头文件以*.hpp
作为扩展名。用户可见的头文件可分为3种:- 一种是综合性导入的头文件,里面仅有其他头文件的导入,这种文件主要是为了方便用户导入相关API。比如GLM将项目分为了4个模块(核心功能、稳定扩展、推荐扩展、实验性扩展),对应的就有两个综合性导入头文件:一个用于导入核心API的
glm.hpp
,一个用于导入其余的扩展API的ext.hpp
,方便用户导入。 - 另一些比较具体的类型导入头文件,这种头文件主要是解决同种类型的头文件较多的情况,比如
vec3.hpp
里面就导入了各种类型的vector3头文件,这些头文件里面主要定义了类型模版的别名,这样使用时就可以不需要填写很长的模版参数了(同时也定义了与GLSL规范相同的类型名称)。 - 还有一种是包含了声明的头文件(主要是函数声明),这些函数均为模版函数(由于参数类型本身带模版)。声明的头文件中并没有实现,在声明头文件的底部引入了
detail/
里面实际的实现。
以上所有的用户可见API均在命名空间
glm
下。 - 一种是综合性导入的头文件,里面仅有其他头文件的导入,这种文件主要是为了方便用户导入相关API。比如GLM将项目分为了4个模块(核心功能、稳定扩展、推荐扩展、实验性扩展),对应的就有两个综合性导入头文件:一个用于导入核心API的
-
核心功能的实现在
detail/
文件夹下。这个文件下的文件扩展名主要就2种:*.hpp
为声明,*.inl
为具体实现。一个例外是glm.cpp
,该文件中显示实例化了各个类的模版,主要是为了能使该库以静态库或动态库的方式使用。以_*.hpp
开头的这些文件是库内部使用一次或多次的头文件,主要实现了一些通用的功能或宏相关的检测处理等:_features.hpp
里主要放了有很多编译器功能检测宏,如果发现了编译器支持某些额外特性,就定义一个GLM_CXX??_???
的宏,表示可以使用该特性。(不过我没有看到有哪个文件使用了这个文件?)_fixes.hpp
里清除了某些编译环境可能预先定义的宏,防止其产生干扰,如max
、min
等。_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_t
是size_t
还是int
)。 -
之前说了函数的声明在根目录下,而函数的定义在
detail/
文件夹下,且均以func_*.inl
作为文件名。在每个*.inl
文件中,可以之前的用户可见函数声明相对应的函数实现。这些实现如果涉及到的只是简单的模版参数,那么在此就直接实现了相关代码;如果涉及到较复杂的实现(例如实现与向量长度有关),那么这段实现中并不会做实际工作,只是首先进行模版参数检测static_assert,然后转发到一个struct的特定静态方法call
去。相应的struct定义在命名空间glm::detail
中,用户一般不可见。通过对向量长度的偏特化,就可以针对不同长度的向量实现特定的方法,而不支持的长度最终会落到一个空的struct中,导致编译错误。struct中的特定实现可能会用到API本身,这个时候就会从上级根目录中导入其头文件。有一些用到了STL库中已有的函数(比如max
),其就在实现中直接通过using std::max
进行导入。对于STL函数作用在向量的每个元素上时,用的就是之前说的_vertorize.hpp
中的向量化,向量化本身的实现与向量长度有关,也是放在偏特化的struct中。 -
向量/矩阵相关的类声明在
detail/type_*.hpp
中,类定义在detail/type_*.inl
中,类定义也有与函数定义类似的转发机制(主要为了处理SIMD相关),转发的目标均在glm::detail
命名空间下,用户不可见。 -
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类型一致。