RL 的源码阅读
前言
距离上次更新有不少时间了,最近工作比较忙,再加上有点生病了,所有晚上回来都挺累,没有时间更新,现在生病好了,继续开始更新。
从math库开始
我们先从src下的math库开始阅读,为啥呢,通过浏览cmakelists.txt文件,然后找到不同源代码项目子目录的顺序,然后依次查找看看,我们会发现,有些源代码作为基石搭在整体项目的底层,其他的代码都通过各种方式引用他们,而他们又不依赖其他的源文件目录的文件,其实就是这些代码没有引用其他写的代码,所有我们优先看这些源代码,从底向上看。
我们先从math的结构开始浏览
math主文件,下面共有metrics、spatial和一下直接放在math更目录的代码文件,然后还有一个CMakeLists.txt文件,我们和之前一样,从CMakeLists.txt开始看起来。
CMakeLists.txt
里面主要是制定了一些list的空间范围和一些编译的信息。有一些值得我们注意的。
-
CMake版本检查:
if(NOT CMAKE_VERSION VERSION_LESS 3.0) add_library(math INTERFACE) add_custom_target(math_sources SOURCES ${HDRS}) else() add_library(math STATIC ${HDRS} dummy.cpp) endif()
- 如果CMake版本不低于3.0,添加一个INTERFACE类型的库
math
,并创建一个自定义目标math_sources
,其源文件为${HDRS}
。 - 如果CMake版本低于3.0,添加一个静态库
math
,其源文件为${HDRS}
和dummy.cpp
。 - INTERFACE主要用于定义编译选项、包含目录和链接库等属性,这些属性会传递给依赖它的目标,它不会生成任何二进制文件,说的直接就是它可以专门用来适用头文件,因为头文件不需要变成成二进制文件,直接使用源代码依赖,然后这个特性是CMake 3.0 才有的,所以我们要进行一次判定,当然如果是新项目可以直接指定Cmake最低版本直接拉到3以上(其实看项目要求,但是我个人认为改更新还是得更新)。当然这里既可以使用静态链接库而不是动态链接库,一般项目是可以任意动态链接或者静态链接,看开发团队的选择。
- 如果CMake版本不低于3.0,添加一个INTERFACE类型的库
-
指针大小检查:
if(CMAKE_SIZEOF_VOID_P EQUAL 4) target_compile_definitions(math INTERFACE -DEIGEN_DONT_ALIGN_STATICALLY) endif()
- 如果指针大小为4字节(即32位系统),为
math
库添加编译定义-DEIGEN_DONT_ALIGN_STATICALLY
。在32位系统上,某些数据结构的内存对齐可能会导致性能问题或崩溃。-DEIGEN_DONT_ALIGN_STATICALLY 编译定义告诉Eigen库不要对静态数据进行对齐优化。在64位系统上因为其对齐要求和机制通常更为宽松,所有不用担心这个问题,这个禁用之后会导致性能下降。
- 如果指针大小为4字节(即32位系统),为
-
CMake版本检查(继续):
if(NOT CMAKE_VERSION VERSION_LESS 3.8) target_compile_features(math INTERFACE cxx_std_11) endif()
- 如果CMake版本不低于3.8,为
math
库设置C++11标准。
- 如果CMake版本不低于3.8,为
-
包含目录设置:
target_include_directories( math INTERFACE $<BUILD_INTERFACE:${rl_SOURCE_DIR}/src> $<INSTALL_INTERFACE:$<INSTALL_PREFIX>/${CMAKE_INSTALL_INCLUDEDIR}/rl-${VERSION}> ${Boost_INCLUDE_DIR} ${EIGEN3_INCLUDE_DIRS} )
- 为
math
库设置包含目录:BUILD_INTERFACE
:构建时使用${rl_SOURCE_DIR}/src
。INSTALL_INTERFACE
:安装时使用${CMAKE_INSTALL_INCLUDEDIR}/rl-${VERSION}
。- 还包括
Boost
和Eigen3
的包含目录。
- 为
metrics目录
metrics被包裹在namespace rl中的namespace math下面。
c++在20之前没有模块机制,在2024年的今天,模块也没有成为一个通用标准,在模块通用之前,使用头文件分离代码,然后使用不同的命名空间分离代码结构是主流的方案。这里采用了一个rl的命名空间,然后定义了一个math空间然后再定义了一个metrics空间。
我们看到这个结构体:
首先定义一个L2,然后在里面将Distance和Size分别定义成泛型T的value_type和size_type的两个别名。当然这里默认了T里面有这两个成员类型,在这里受限于当时的语法和标准,这里面我们目前是看到没有任何限制的,也就是如果在这一层的泛型T泛化成没有value_type和size_type的对象,程序会错误,而且这种错误一般隐藏的非常非常深。
template<typename T>
struct L2
{
typedef typename T::value_type Distance;
typedef typename T::size_type Size;
typedef T Value;
Distance operator()(const Value& lhs, const Value& rhs) const
{
using ::std::begin;
using ::std::end;
auto first1 = begin(lhs);
auto last1 = end(lhs);
auto first2 = begin(rhs);
Distance distance = Distance();
while (first1 != last1)
{
Distance tmp = *first1 - *first2;
distance += tmp * tmp;
++first1;
++first2;
}
return ::std::sqrt(distance);
}
Distance operator()(const Distance& lhs, const Distance& rhs, const Size& index) const
{
return ::std::abs(lhs - rhs);
}
};
这里可以使用 static_assert
或者 SFINAE(Substitution Failure Is Not An Error)技术来进行编译时检查。
使用 static_assert
进行编译时检查
你可以在结构体内部添加 static_assert
来检查 T
是否具有所需的成员类型:
template<typename T>
struct L2
{
static_assert(std::is_same<typename T::value_type, typename T::value_type>::value, "T must have a member type named value_type");
static_assert(std::is_same<typename T::size_type, typename T::size_type>::value, "T must have a member type named size_type");
typedef typename T::value_type Distance;
typedef typename T::size_type Size;
typedef T Value;
Distance operator()(const Value& lhs, const Value& rhs) const
{
using ::std::begin;
using ::std::end;
auto first1 = begin(lhs);
auto last1 = end(lhs);
auto first2 = begin(rhs);
Distance distance = Distance();
while (first1 != last1)
{
Distance tmp = *first1 - *first2;
distance += tmp * tmp;
++first1;
++first2;
}
return ::std::sqrt(distance);
}
Distance operator()(const Distance& lhs, const Distance& rhs, const Size& index) const
{
return ::std::abs(lhs - rhs);
}
};
使用 SFINAE 进行编译时检查
也可以使用 SFINAE 来进行更灵活的检查:
template<typename T, typename = void>
struct has_value_type : std::false_type {};
template<typename T>
struct has_value_type<T, std::void_t<typename T::value_type>> : std::true_type {};
template<typename T, typename = void>
struct has_size_type : std::false_type {};
template<typename T>
struct has_size_type<T, std::void_t<typename T::size_type>> : std::true_type {};
template<typename T>
struct L2
{
static_assert(has_value_type<T>::value, "T must have a member type named value_type");
static_assert(has_size_type<T>::value, "T must have a member type named size_type");
typedef typename T::value_type Distance;
typedef typename T::size_type Size;
typedef T Value;
Distance operator()(const Value& lhs, const Value& rhs) const
{
using ::std::begin;
using ::std::end;
auto first1 = begin(lhs);
auto last1 = end(lhs);
auto first2 = begin(rhs);
Distance distance = Distance();
while (first1 != last1)
{
Distance tmp = *first1 - *first2;
distance += tmp * tmp;
++first1;
++first2;
}
return ::std::sqrt(distance);
}
Distance operator()(const Distance& lhs, const Distance& rhs, const Size& index) const
{
return ::std::abs(lhs - rhs);
}
};
让我们继续往下看:这里就是对矩阵进行了一个偏特化,传入一个行向量的矩阵,其中这里的Distance就修正为其的范数,norm()函数用于计算矩阵或向量的范数,具体就是各个元素平方和的平方根,下面是返回Distance直接距离。这里也是一样,没有对L2传入的模板参数进行强校验,也就是进行约束。我们一样可以采用上面的几种方式对其进行约束。
template<typename Scalar, int Rows, int Options, int MaxRows, int MaxCols>
struct L2< ::Eigen::Matrix<Scalar, Rows, 1, Options, MaxRows, MaxCols>>
{
typedef Scalar Distance;
typedef int Size;
typedef ::Eigen::Matrix<Scalar, Rows, 1, Options, MaxRows, MaxCols> Value;
Distance operator()(const Value& lhs, const Value& rhs) const
{
return (lhs - rhs).norm();
}
Distance operator()(const Distance& lhs, const Distance& rhs, const Size& index) const
{
return ::std::abs(lhs - rhs);
}
};
这一段和上面没有什么区别,重点在与多了一个*:
template<typename Scalar, int Rows, int Options, int MaxRows, int MaxCols>
struct L2< ::Eigen::Matrix<Scalar, Rows, 1, Options, MaxRows, MaxCols>*>
{
typedef Scalar Distance;
typedef int Size;
typedef ::Eigen::Matrix<Scalar, Rows, 1, Options, MaxRows, MaxCols>* Value;
Distance operator()(const Value& lhs, const Value& rhs) const
{
return (*lhs - *rhs).norm();
}
Distance operator()(const Distance& lhs, const Distance& rhs, const Size& index) const
{
return ::std::abs(lhs - rhs);
}
};
在L2Squared中也是一样的,区别在于将L2中的返回值都返回其值平方,我个人是感觉这两个头文件可以合并起来。