2021SC@SDUSC
开源游戏引擎 Overload 代码模块分析 之 OvTools(六)—— Utils(下)
目录
前言
本篇是开源游戏引擎 Overload 模块 OvTools 的 Utils 小模块的下篇,也是 Utils 的最后一篇,但不是 OvTools 的终篇。Utils(上)可前往上上篇文章查看;Utils(中)可前往上篇文章查看。而本篇,笔者将探究 Utils 的最后两部分:String 与 SystemCalls。
另外,想先大致了解 Overload 可前往这篇文章,想看其他相关文章请前往笔者的 Overload 专栏自主选择。
分析
1、String
1.1 String.h
1.1.1 头文件
#include <string>
#include <functional>
string 不必多说;functional 已讲述过,请自行回顾。
1.1.2 主体代码
这是一个 string 类,但不是 C++ 库的 string 型,所以很重要的是 namespace:
namespace OvTools::Utils
而这个 string 类的官方注释的描述却是处理随机数生成,其定义的代码如下:
class String
{
public:
/**
* Disabled constructor
*/
String() = delete;
/**
* Replace the first occurence of p_from by p_to in the given string
* @param p_target (The string to modify)
* @param p_from
* @param p_to
*/
static bool Replace(std::string& p_target, const std::string& p_from, const std::string& p_to);
/**
* Replace every occurence of p_from by p_to in the given string
* @param p_target (The string to modify)
* @param p_from
* @param p_to
*/
static void ReplaceAll(std::string& p_target, const std::string& p_from, const std::string& p_to);
/**
* Generate a unique string satisfying the availability predicate
* @param p_source
* @param p_isAvailable (A callback that must returning true if the input string is available)
*/
static std::string GenerateUnique(const std::string& p_source, std::function<bool(std::string)> p_isAvailable);
};
构造函数的 delete 不多说了,可自行回顾;而其他函数,从这些注释中却看到都是在处理字符串,与官方描述仿佛不符合。所以我们需要到 String.cpp 中去一探究竟。
1.2 String.cpp
头文件只有上文的 String.h ,所以让我们直接看三个函数吧:
Replace() 函数
bool OvTools::Utils::String::Replace(std::string & p_target, const std::string & p_from, const std::string & p_to)
{
size_t start_pos = p_target.find(p_from);
if (start_pos != std::string::npos)
{
p_target.replace(start_pos, p_from.length(), p_to);
return true;
}
return false;
}
该函数作用是只替换检索到的第一段要替换的字符串,它先是调用了 C++ string 的 find() 函数,得到要改写的 p_from 字符串在 p_target 中的位置,赋值给 size_t 型的变量 start_pos;其中,size_t 是 unsigned __int64 型的重命名,也是 find() 函数的唯一的返回类型。但是,如果没有找到,就会返回下述的 npos 标记:
在 if 的判断中,函数用到了 string::npos 静态成员常量。该常量有三种常见的用法:
1、表示 size_t 型的元素具有的最大可能值;
2、当这个值是关于字符串时,该值表示的是字符串的结尾;
3、作为表明没有匹配的返回值。
显然,此处若找到了,其含义就是字符串的结尾,最后直接用 C++ string 的 replace() 函数按需替换字符串并返回 true;当然,如果是没找到,start_pos 是 npos,就返回 false了。
ReplaceAll() 函数
void OvTools::Utils::String::ReplaceAll(std::string& p_target, const std::string& p_from, const std::string& p_to)
{
if (p_from.empty()) return;
size_t start_pos = 0;
while ((start_pos = p_target.find(p_from, start_pos)) != std::string::npos)
{
p_target.replace(start_pos, p_from.length(), p_to);
start_pos += p_to.length();
}
}
该函数和 Replace() 函数原理差不多,不过是替换所有要替换的字符串,所以 while 循环条件中调用的 find() 函数有两个参数,表示第二个参数作为起始位开始寻找字符串,此处初值是 0 ,start_pos 会检索到第一个要替换的字符串。
接着 while 中的第一轮操作,在 replace() 替换掉字符串后,start_pos 会增加,使位置刚好到替换掉的字符串的尾;然后新一轮循环,start_pos 就在 find() 中作为新的起始位。所以该 while 循环的功能就是不断检索到要替换的字符串,替换后将检索起始位置标到字符串尾巴,再检索,再替换 …… 直至目标字符串被全部检索完毕。
GenerateUnique() 函数
该函数功能是生成唯一可用断言,代码非常长,我们一段一段看吧:
std::string OvTools::Utils::String::GenerateUnique(const std::string& p_source, std::function<bool(std::string)> p_isAvailable)
{
先看传入的参数,string 型 p_source,和来自 functional 头文件的一个类模板 function,一个可调用对象的包装器。它可以在统一包装器中包含各种可调用对象,例如函数、表达式或其他函数对象,指向成员函数、数据成员的指针等等;另外,如果没有可调用目标,那么调用它时会抛出一个异常 std::bad_function_call。在本函数中,该类的对象将用于判断字符串是否可用。
接着,该函数先声明了下列几个变量:
auto suffixlessSource = p_source;
auto suffixOpeningParenthesisPos = std::string::npos;
auto suffixClosingParenthesisPos = std::string::npos;
// Keep track of the current character position when iterating onto `p_source`
auto currentPos = decltype(std::string::npos){p_source.length() - 1};
前三个变量不用多说,npos 在上文已有提及。第四个变量 currentPos,根据注释,它的目的是在 p_source 迭代时,跟踪当前字符的位置。该变量使用了关键词 decltype,类型说明符,它的作用是选择并返回操作数的数据类型,此处显然是返回了 size_t 型,并且变量 currentPos 指向 p_source 的末尾。
现在来到函数的第一个循环,目的是寻找 “( ” 、“ )” 符号位置:
// Here we search for `(` and `)` positions. (Needed to extract the number between those parenthesis)
for (auto it = p_source.rbegin(); it < p_source.rend(); ++it, --currentPos)
{
const auto c = *it;
if (suffixClosingParenthesisPos == std::string::npos && c == ')') suffixClosingParenthesisPos = currentPos;
if (suffixClosingParenthesisPos != std::string::npos && c == '(') suffixOpeningParenthesisPos = currentPos;
}
循环条件中用到了反向迭代器 rbegin(),之前的文章已有简要说明;循环内部,npos 表示字符串结尾位置,其余很简单,不多赘述。
之后函数声明了一个变量 counter,将用于存取括号内的数字;此处默认为 “ 1 ”,表示如果未找到括号:
// We need to declare our `counter` here to store the number between found parenthesis OR 1 (In the case no parenthesis, AKA, suffix, has been found)
auto counter = uint32_t{ 1 };
此处,uint32_t 型是重命名了 unsigned int 型。
接着,是一个有点复杂的 if 语句:
// If the two parenthis have been found AND the closing parenthesis is the last character AND there is a space before the opening parenthesis
if (suffixOpeningParenthesisPos != std::string::npos && suffixClosingParenthesisPos == p_source.length() - 1 && suffixOpeningParenthesisPos > 0 && p_source[suffixOpeningParenthesisPos - 1] == ' ')
{
// Extract the string between those parenthesis
const auto between = p_source.substr(suffixOpeningParenthesisPos + 1, suffixClosingParenthesisPos - suffixOpeningParenthesisPos - 1);
// If the `between` string is composed of digits (AKA, `between` is a number)
if (!between.empty() && std::find_if(between.begin(), between.end(), [](unsigned char c) { return !std::isdigit(c); }) == between.end())
{
counter = static_cast<uint32_t>(std::atoi(between.c_str()));
suffixlessSource = p_source.substr(0, suffixOpeningParenthesisPos - 1);
}
}
auto result = suffixlessSource;
首先,if 判断如果左括号非结尾位(即找到了双括号),右括号位于最后,且左括号非起始位,其前一位有空格,则通过判断;其次,调用库函数 substr 复制括号内的字符串(不包括两括号)存储在变量 between 中。
然后,又一个内部 if 判断,其中用到了库函数 find_if:同 find() 一样,能在给出的序列范围内依次查询,当第一次遇到能使给出的表达式为 true 的元素时,返回该元素;如果没有找到,会返回给出序列的结束迭代器。另外还用到了 isdigit(),该函数主要用于检查传入参数是否为十进制数字字符。所以,该 if 判断如果 between 非空且 between 内是非数字的字符是 beteen 的结束迭代器(即相当于全是数字字符),则通过。
判断的内部,使用了 c_str() 获得 between 字符串指针常量,atoi() 转换其为整型数,static_cast<> 再强制转换为 uint32_t 即 unsigned int 型,并赋值给 counter;而变量 suffixlessSource 获得左括号之前的子字符串,并赋值给变量 result。
在这之后,该函数只剩一个 while 循环来检测 result :
// While `result` isn't available, we keep generating new strings
while (!p_isAvailable(result))
{
// New strings are composed of the `suffixlessSource` (Ex: "Foo (1)" without suffix is "Foo")
result = suffixlessSource + " (" + std::to_string(counter++) + ")";
}
return result;
}
while 使用了 function 类对象 p_isAvailable 的括号 “()” 重载函数,判断参数是否可用。若不可用则进入循环,并将 suffixlessSource 添加上括号包裹的字符串型的新数字后缀,重新生成新的字符串后赋值 result。最后返回 result,完成任务。
现在,看完了所有的 String 类的函数,笔者觉得官方对该类的作用注释应该是出错了,用成了 Random 类的注释;此外,从其他探究 Overload 引擎的队员反馈中也得到了证实,其它模块也存在一些注释错误。
接下来,就是最后一个部分 SystemCalls:
2、SystemCalls
2.1 SystemCalls.h
该文件仅有 string 头文件,不多说,直接看主体代码,SystemCalls 类:
SystemCalls 类
class SystemCalls
{
public:
/**
* Disabled constructor
*/
SystemCalls() = delete;
/**
* Open the windows explorer at the given path
* @param p_path
*/
static void ShowInExplorer(const std::string& p_path);
/**
* Open the given file with the default application
* @param p_file
* @param p_workingDir
*/
static void OpenFile(const std::string& p_file, const std::string & p_workingDir = "");
/**
* Open the given file for edition with the default application
* @param p_file
*/
static void EditFile(const std::string& p_file);
/**
* Open the given url with the default browser
* @param p_url
*/
static void OpenURL(const std::string& p_url);
};
该类就像之前文章所说的,集中于函数功能,所以构造函数默认删除;其他函数的功能都有注释,不多赘述,让我们到 SystemCalls.cpp 看它们的具体实现方法:
2.2 SystemCalls.cpp
2.2.1 头文件
#include "OvTools/Utils/PathParser.h"
#include "OvTools/Utils/SystemCalls.h"
#include <Windows.h>
该文件不仅包含了上述的 SystemCalls.h,还包含了同是 Utils 小模块的另一个部分 PathParser 的头文件 PathParser.h。略有遗忘或还没有阅读过 PathParser 的读者可以前往Utils(上)查看。其次,文件还包括了 Windows.h,Windows 程序的必备头文件之一,涉及了 Windows 内核 API,图形界面接口,图形设备函数等重要功能。
2.2.2 主体代码
该文件主要对上面声明的四个函数做了具体定义。
ShowInExplorer() 函数
void OvTools::Utils::SystemCalls::ShowInExplorer(const std::string & p_path)
{
ShellExecuteA(NULL, "open", OvTools::Utils::PathParser::MakeWindowsStyle(p_path).c_str(), NULL, NULL, SW_SHOWNORMAL);
}
首先调用了 PathParser 类的函数 MakeWindowsStyle,使得 p_path 路径格式符合 Windows 路径格式;而后 c_str 获得字符串指针常量;最后调用 Windows.h 的函数 ShellExecuteA,该函数能对指定的文件执行操作,此处是将路径指向的文件以原本的大小和位置打开窗体。
其中,ShellExecuteA 函数有多达六个参数,其意义从左到右分别是:
1、hwnd:父窗口的句柄,用于显示 UI 或错误消息;如果操作与窗口没有关联,则此值可以为 NULL。
2、lpOperation:指向空终止字符串的指针,指定要执行的动作。此处代码的 open 即为打开文件。
3、lpFile:指向空终止字符串的指针,指定要执行操作的文件或对象。
4、lpParameters:如果 lpFile 是一个可执行文件,则此参数是指向空终止字符串的指针,其字符串指定要传递给应用程序的参数;如果 lpFile 是一个文档文件,则此参数为 NULL。
5、lpDirectory:指向空终止字符串的指针,指定操作的默认工作目录;如果该值为 NULL,则使用当前工作目录。
6、nShowCmd:指定应用程序打开时如何显示。此处代码的 SW_SHOWNORMAL 含义是:以原本的大小和位置,激活并显示指定的窗体。
关于 ShellExecuteA 函数更多的信息及其各参数的具体用法,此处就不再赘述了,想了解的读者请前往官方文档。
OpenFile() 函数
void OvTools::Utils::SystemCalls::OpenFile(const std::string & p_file, const std::string & p_workingDir)
{
ShellExecuteA(NULL, "open", OvTools::Utils::PathParser::MakeWindowsStyle(p_file).c_str(), NULL,
p_workingDir.empty() ? NULL : OvTools::Utils::PathParser::MakeWindowsStyle(p_workingDir).c_str(),
SW_SHOWNORMAL);
}
该函数与上面的 ShowInExplorer() 函数代码大同小异,只是参数 IpDirectory 的值使用了一个三目运算符:如果 p_workingDir 是空的(即未传入工作目录,默认赋值空),则为 IpDirectory 为 NULL,即当前工作目录;否则为 p_workingDir 给出的目录。
EditFile() 函数
void OvTools::Utils::SystemCalls::EditFile(const std::string & p_file)
{
ShellExecuteW(NULL, NULL, std::wstring(p_file.begin(), p_file.end()).c_str(), NULL, NULL, SW_NORMAL);
}
该函数与上述的两个函数道理也是一样的。此处使用的 ShellExecuteW() 的参数含义和上面的 ShellExecuteA 是相同的,其第二个参数 lpOperation 值虽然为 NULL,但是默认会自动替换为 open。具体用法请查看官方文档。
OpenURL() 函数
void OvTools::Utils::SystemCalls::OpenURL(const std::string& p_url)
{
ShellExecute(0, 0, p_url.c_str(), 0, 0, SW_SHOW);
}
该函数原理也一样,用到的 ShellExecute 的功能也和上述两个函数差不多。其中,参数里的 0 类似其他两个函数的 NULL;SW_SHOW 的含义是激活窗体,并将其显示在当前的大小和位置上。
上述的四个函数都用到了 ShellExecute 系列的函数,这些函数功能上相差无几,且与 Overload 系列文章联系也不是很大,所以此处就不多探究具体的区别,烦请想了解的读者自行前往官方文档寻找比较。
总结
String 与 SystemCalls 的各个函数其实都不复杂,都是使用已有库函数简单完成一些字符串或是系统回调的处理工作,理解起来很快。
至此,Overload 游戏引擎 OvTools 模块的所有主体内容已经较为详细地分析探究完了,想了解更深的读者请到笔者的这篇文章查看 Overload 源码的下载地址以及参考可能遇到的安装问题。另外,考虑到本篇的篇幅问题,笔者计划下一篇将是 OvTools 的终篇 —— OvTools 模块总结。