<2021SC@SDUSC>
开源游戏引擎 Overload 代码模块分析 之 OvTools(五)—— Utils(中)
目录
前言
本篇是开源游戏引擎 Overload 模块 OvTools 的 Utils 小模块的中篇。简单回顾一下,上篇已经分析了 Utils 六个部分中的 PathParser 与 Random,它们分别是负责路径处理和随机数生成的类,具体可前往上篇文章查看。本篇咱们就继续探究接下来两个部分:ReferenceOrValue 与 SizeConverter。
另外,想先大致了解 Overload 可前往这篇文章,想看其他相关文章请前往笔者的 Overload 专栏自主选择。
分析
1、ReferenceOrValue
1.1 ReferenceOrValue 类
1.1.1 头文件
#include <variant>
该头文件来自 C++ 标准库,实现的功能主要是对变量对象的值的保存和管理。文件中的核心概念就是 variant 类型,其中文翻译可以是变体。
具体来说,variant 数据类型是所有没被显式声明(用如 Dim、Private、Public 或 Static等语句)为其他类型变量的数据类型,它没有类型声明字符。除了定长 String 数据及用户定义类型外,variant 可以包含任何种类的数据,包括 Empty、Error、Nothing 及 Null 等特殊值。所以,variant 类型的变量所包含的值类型必须是赋予了 variant 模板参数的类型之一,我们把这些模板参数称为替代项。
此外,文件还含有多种例如运算符重载等处理变量对象值的函数,可以用 VarType 函数或 TypeName 函数来决定如何处理 variant 中的数据。
1.1.2 主体代码
主体代码是一个 ReferenceOrValue 类,其作用是简化一个变量类型的定义,这样就不用在意这个变量是引用还是实值等等了。由于该类的操作都比较简洁,所以没有另设 cpp 文件,函数实现代码及其注释都已经在 h 文件,让我们来直接看看代码吧:
template <typename T>
class ReferenceOrValue
{
public:
/**
* Construct the ReferenceOrValue instance with a reference
* @param p_reference
*/
ReferenceOrValue(std::reference_wrapper<T> p_reference) : m_data{ &p_reference.get() }
{
}
/**
* Construct the ReferenceOrValue instance with a value
* @param p_value
*/
ReferenceOrValue(T p_value = T()) : m_data{ p_value }
{
}
/**
* Make the ReferenceOrValue a reference
* @param p_reference
*/
void MakeReference(T& p_reference)
{
m_data = &p_reference;
}
/**
* Make the ReferenceOrValue a value
* @param p_value
*/
void MakeValue(T p_value = T())
{
m_data = p_value;
}
/**
* Implicit conversion of a ReferenceOrValue to a T
*/
operator T&()
{
return Get();
}
/**
* Assignment operator thats call the setter of the ReferenceOrValue instance
* @param p_value
*/
ReferenceOrValue<T>& operator=(T p_value)
{
Set(p_value);
return *this;
}
/**
* Returns the value (From reference or directly from the value)
*/
T& Get() const
{
if (auto pval = std::get_if<T>(&m_data))
return *pval;
else
return *std::get<T*>(m_data);
}
/**
* Sets the value (To the reference or directly to the value)
* @param p_value
*/
void Set(T p_value)
{
if (auto pval = std::get_if<T>(&m_data))
* pval = p_value;
else
*std::get<T*>(m_data) = p_value;
}
private:
std::variant<T, T*> m_data;
};
该类大部分函数的实现都很简单且有功能注释,就不多赘述了。其中简单一提几个调用的函数操作:std::get_if 与 std::get,这两个函数都是来自上面提到的 variant 文件。两者都是获取对象的变体,只是前者多了一个判断是否存在值,若不存在则返回 nullptr,该类型将被 if 语句判断为类似 0 的 false 值。
了解了上述的两个标准库函数,ReferenceOrValue 类的函数就没有什么难度了。所以 ReferenceOrValue 部分的内容都较简单,无非是 variant 的使用,较好理解。所以让我们直接继续下一个部分吧:
2、SizeConverter
2.1 SizeConverter.h
2.1.1 头文件
#include <cstdint>
#include <tuple>
#include <string>
string 文件不多说;cstdint 文件包含了 stdint.h 并将关联名称添加到 std 命名空间,还能确保使用 std 中的外部链接声明的名称在 std 命名空间中声明;tuple 文件定义了一个模板 tuple,它的实例包括不同类型的对象。
2.1.2 主体代码
文件主体代码包含了一个 SizeConverter 类,该类可以换算字节单位,代码如下:
class SizeConverter
{
public:
enum class ESizeUnit
{
BYTE = 0,
KILO_BYTE = 3,
MEGA_BYTE = 6,
GIGA_BYTE = 9,
TERA_BYTE = 12
};
/**
* Disabled constructor
*/
SizeConverter() = delete;
/**
* Converts the given size to the optimal unit to avoid large numbers (Ex: 1000B will returns 1KB)
* @param p_value
* @param p_unit
*/
static std::pair<float, ESizeUnit> ConvertToOptimalUnit(float p_value, ESizeUnit p_unit);
/**
* Converts the given size from one unit to another
* @param p_value
* @param p_from
* @param p_to
*/
static float Convert(float p_value, ESizeUnit p_from, ESizeUnit p_to);
/**
* Converts the given unit to a string
* @param p_unit
*/
static std::string UnitToString(ESizeUnit p_unit);
};
这里有函数的声明及功能注释,都是些类型转换操作,不多赘述。其中注意 public 变量 ESizeUnit 是一个枚举类,包含的是从 B、KB 到 TB 的字节单位;另外,该类的构造函数使用了 delete 默认删除,以前的文章也有谈及。现在让我们到 cpp 文件中看函数们的具体定义:
2.2 SizeConverter.cpp
该文件除了上方的 h 文件,没有其他头文件;文件具体定义了 SizeConverter 类的三个函数。此处笔者按照函数之间的互相调用顺序依次列出:
Convert() 函数
float OvTools::Utils::SizeConverter::Convert(float p_value, ESizeUnit p_from, ESizeUnit p_to)
{
const float fromValue = powf(1024.0f, static_cast<float>(p_from) / 3.0f);
const float toValue = powf(1024.0f, static_cast<float>(p_to) / 3.0f);
return p_value * (fromValue / toValue);
}
该函数传入的参数含义分别是要修改的值、原字节单位、新字节单位,且两个单位都是来自类内的枚举类 ESizeUnit;接着函数调用 static_cast<> 操作,将两个单位强制转换为 float 型,除以 3.0f 求出幂次后,用 corecrt_math.h 的 powf() 幂运算函数计算以 1024.0f 为底数的比特值,这样就得到了两个单位之间的进制;最后求比值进行换算。
ConvertToOptimalUnit() 函数
std::pair<float, OvTools::Utils::SizeConverter::ESizeUnit> OvTools::Utils::SizeConverter::ConvertToOptimalUnit(float p_value, ESizeUnit p_unit)
{
if (p_value == 0.0f) return { 0.0f, ESizeUnit::BYTE };
const float bytes = Convert(p_value, p_unit, ESizeUnit::BYTE);
const int digits = static_cast<int>(trunc(log10(bytes)));
const ESizeUnit targetUnit = static_cast<ESizeUnit>(fmin(3.0f * floor(digits / 3.0f), static_cast<float>(ESizeUnit::TERA_BYTE)));
return { Convert(bytes, ESizeUnit::BYTE, targetUnit), targetUnit };
}
该函数能换算原单位为自判定最佳单位,最佳的依据为使除去单位后的数字最小。学习了 Convert() 函数,这个函数的操作也很明朗了。在判断给出的值是非零后,代码调用 Convert() 先换算为 ESizeUnit 中最小的单位 BYTE,接着调用 log10() 求该值的对数。但是显然,该值不一定就是 10 的整次幂,所以要调用 trunc() 取整,并将其用 static_cast 强制转换为 int 型存在 digits 变量中。
得到了幂数,就可以计算适用的单位了。由于比特单位间的进制是以千即 103 为基础,digits 需要除以 3.0f 并调用 floor() 向下取整,再乘以 3.0f 得到的值才符合 ESizeUnit 中的计量方式;最后调用 fmin() 得到该幂次与 ESizeUnit 最大单位 TERA_BYTE 两者中的较小者,防止溢出,这样就获得了最优单位,再调用 Convert() 换算就好了。
简单一提,上述的多个数学运算函数都来自 corecrt_math.h 文件。该文件属于 std 的 math.h。
UnitToString() 函数
std::string OvTools::Utils::SizeConverter::UnitToString(ESizeUnit p_unit)
{
switch (p_unit)
{
case OvTools::Utils::SizeConverter::ESizeUnit::BYTE: return "B";
case OvTools::Utils::SizeConverter::ESizeUnit::KILO_BYTE: return "KB";
case OvTools::Utils::SizeConverter::ESizeUnit::MEGA_BYTE: return "MB";
case OvTools::Utils::SizeConverter::ESizeUnit::GIGA_BYTE: return "GB";
case OvTools::Utils::SizeConverter::ESizeUnit::TERA_BYTE: return "TB";
}
return "?";
}
最后这个函数更加简单了,用 switch 语句判断给出单位类型,返回成我们缩写的单位,例如 MEGA_BYTE 输出为 MB。
总结
由此可见,ReferenceOrValue 与 SizeConverter 两部分并不难,都是些标准库函数调用。但是,我们要学习这些方法的实现逻辑,好让自己的代码更加简明。
下一篇,笔者将探究 Utils 的最后两部分:String 与 SystemCalls。如果篇幅不长,这将不仅是 Utils 的最后一篇,也是 OvTools 模块分析的最后一篇,笔者会直接在篇尾对 OvTools 做一个大体总结;太长的话,还是考虑总结单独写一篇吧。