写于 2023 年 3 月 17 日。
重新拾起 C++ 开发已经有 8 个月了,自己主导做的一件事情。
三个看法,一是记住 C++ 能不造轮子,就别造轮子了;二是能做单元测试的东西,就要做单元测试;三是好好利用 ChatGPT 之类的东西来优化代码 -/- doge
起因
被分配了一个任务,用现今的 C++ 标准改写一个原先用 Qt 编写的代码。就是要去除掉那些以 Q 开头的代码(QString
、QVariant
、QSetting
和 QFile
等等)。
重新用 C++ 标准改写的这份代码,扩展性就会更强,当然 Qt 的生态也不错,不过是业务需求如此罢了,不展开具体内容。
开始「 模仿 」
这份 Qt 编写的代码中有自己的文件读写工具类 FileTools
(是根据实际业务产生的,即各种规定好的读写要求,怎么读取,怎么写入数据等等)。
用 Qt 构建的
FileTools
类, 其代码长度大概为两千多行左右
只不过这个类是基于 Qt 整个语言生态构造的,现在打算要去除 Qt 罢了。
我一开始,就是简单仿造这个 FileTools
类,函数名大致不变,函数内部的处理逻辑也大致不变,入参和返回值的类型由 Qt 的数据结构改成 C++ 标准下的,例如 QString
用 std::string
代替。
但其实,在修改这个 FileTools
类时,发现这其实算不上是个类,只是把功能函数全当作成员函数罢了,要使用这些函数时,就用这个类创建一个对象,通过这个对象来调用所需的功能函数。
大概是刚从前端转到做 C++ 的开发,我一开始也觉得没什么问题,就是 函数式编程
嘛。
但是 C++ 的一大精髓就是 OOP(面向对象开发),这个类弄得更像面向过程的。
且这个类里有蛮多方法其实可以继承复用的,阅读其中的代码,有点乱。
越开发,越能闻到代码传出的那股 「 坏味道 」。
该换思路来做。
开始「 包轮子 」
包包包,有现成的,优秀的,经过试验和测试的功能函数,我是绝对不会重复造轮子的,我是个很懒的开发者 /doge。
测试驱动开发
Test-Driven Development (TDD)
整个项目使用 GoogleTest 来做单元测试,开发过程中,先编写测试用例,然后再编写代码,最后再运行测试用例,看看是否通过,不通过,则修改代码,直到通过为止。
记住,测试代码应当与你功能代码的「 体量 」差不多大。
UFile
重新拾起 C++ 开发,看了蛮多 C++ 的书籍(《clean C++》、《Effective C++》等),以及看 cppreference
官网内容,感觉要做的文件读写工作。一些功能函数可以基于 C++ 标准提供的 <filesystem>
文件系统库来做。
UFile
#ifndef UFILE_HPP_
#define UFILE_HPP_
#include <iostream>
#include <string>
#include "Tools.hpp"
#if defined(__clang__) || defined(__GNUC__)
#define CPP_STANDARD __cplusplus
#elif defined(_MSC_VER)
#define CPP_STANDARD _MSVC_LANG
#endif
#if CPP_STANDARD >= 201103L && CPP_STANDARD < 201703L
#include <cstring>
#include <experimental/filesystem>
namespace fs = std::experimental::filesystem;
#endif
#if CPP_STANDARD >= 201703L
#include <filesystem>
namespace fs = std::filesystem;
#endif
class UFile {
private:
fs::path _p;
public:
explicit UFile(const std::string &path) : _p(path) {}
explicit UFile(const fs::path &path) : _p(path) {}
std::string path() { return _p.string(); }
};
#endif // UFILE_HPP_
将 <filesystem>
的 path
类作为私有变量使用;
因为要适配 Linux 和 Windows 平台以及不同的 C++ 标准,加个宏定义来做判断。
这样就是一个基类,接下来对各种类型处理的文件类都继承该基类。
TxtFile
#ifndef TXTFILE_HPP_
#define TXTFILE_HPP_
#include <fstream>
#include "UFile.hpp"
class TxtFile : public UFile {
private:
std::string _data;
public:
explicit TxtFile(const std::string& path) : UFile(path) {}
explicit TxtFile(const fs::path& path) : UFile(path) {}
std::string getData() const { return _data; }
void setData(const std::string& data) { _data = data; }
bool readData() {
std::ifstream file(path());
if (!file) {
return false;
}
_data.assign((std::istreambuf_iterator<char>(file)),
std::istreambuf_iterator<char>());
file.close();
return true;
}
bool writeData() {
std::ofstream file(path());
if (!file) {
return false;
}
file << _data;
return true;
}
bool appendWriteData(const std::string& data) {
std::ofstream file(path(), std::ios_base::app);
if (!file) {
return false;
}
file << data;
return true;
}
};
#endif // TXTFILE_HPP_
文本文件主要是对字符串的处理,根据需求考虑读取,写入和追加写入这三个功能。
就是调用 <fstream>
这个库中的 std::ifstream
和 std::ofstream
类对文本文件做处理,有个数据流的概念,那三个功能就是对数据流做些处理即可。
IniFile
#ifndef INIFILE_HPP_
#define INIFILE_HPP_
#include "UFile.hpp"
#include "ini/SimpleIni.h"
class IniFile : public UFile {
private:
CSimpleIniA _ini;
public:
explicit IniFile(const std::string &path) : UFile(path) {}
explicit IniFile(const fs::path &path) : UFile(path) {}
~IniFile() {}
bool setup() {
_ini.SetUnicode();
SI_Error rc = _ini.LoadFile(path().c_str());
if (rc < 0) return false;
return true;
}
void getFromIni(const std::string §ion, const std::string &key,
std::string ¶m, const char *defaultVal) {
param = _ini.GetValue(section.c_str(), key.c_str(), defaultVal);
}
template <typename T>
void getFromIni(const std::string §ion, const std::string &key, T ¶m,
const T defaultVal) {
std::string paramType = typeid(T).name();
std::string tempParam;
tempParam = _ini.GetValue(section.c_str(), key.c_str(),
std::to_string(defaultVal).c_str());
if (paramType[0] == 'i')
param = static_cast<T>(stoi(tempParam));
else if (paramType[0] == 'f')
param = static_cast<T>(stof(tempParam));
else if (paramType[0] == 'd')
param = static_cast<T>(stod(tempParam));
else if (paramType[0] == 'b')
if (tempParam == "false" || tempParam == "0")
param = false;
else if (tempParam == "true" || tempParam == "1")
param = true;
}
template <typename T>
void getFromIni(const std::string §ion, const std::string &key, T *param,
const T *defaultVal, const int &size) {
int index = 0;
std::string paramType = typeid(T).name();
if (_ini.GetValue(section.c_str(), key.c_str()) == nullptr)
while (index <= size - 1) {
param[index] = defaultVal[index];
index++;
}
else {
std::string tempParamArrayStr =
_ini.GetValue(section.c_str(), key.c_str());
std::vector<std::string> tempParamArray =
split(tempParamArrayStr, " ,\t\n");
if (paramType[0] == 'i')
for (int i = 0; i < tempParamArray.size(); ++i)
param[index++] = static_cast<T>(stoi(tempParamArray[i]));
else if (paramType[0] == 'f')
for (int i = 0; i < tempParamArray.size(); ++i)
param[index++] = static_cast<T>(stof(tempParamArray[i]));
else if (paramType[0] == 'd')
for (int i = 0; i < tempParamArray.size(); ++i)
param[index++] = static_cast<T>(stod(tempParamArray[i]));
while (index <= size - 1) {
param[index] = defaultVal[index];
index++;
}
}
}
void setToIni(const std::string §ion, const std::string &key,
const char *fromValue) {
_ini.SetValue(section.c_str(), key.c_str(), fromValue);
}
template <typename T>
void setToIni(const std::string §ion, const std::string &key,
const T fromValue) {
std::string valueType = typeid(T).name();
std::string toValue;
if (valueType[0] == 'i')
toValue = std::to_string(fromValue);
else if (valueType[0] == 'f')
toValue = std::to_string(fromValue);
else if (valueType[0] == 'd')
toValue = std::to_string(fromValue);
else if (valueType[0] == 'b')
if ((bool)fromValue == false)
toValue = "false";
else if ((bool)fromValue == true)
toValue = "true";
_ini.SetValue(section.c_str(), key.c_str(), toValue.c_str());
}
template <typename T>
void setToIni(const std::string §ion, const std::string &key,
const T *fromValueArr, const int &size) {
if (size <= 0) return;
std::string valueType = typeid(T).name();
std::string toValueArr;
if (valueType[0] == 'i')
for (int i = 0; i < size; ++i) {
toValueArr += std::to_string(fromValueArr[i]);
if (i != size - 1) toValueArr += ", ";
}
else if (valueType[0] == 'f')
for (int i = 0; i < size; ++i) {
toValueArr += std::to_string(fromValueArr[i]);
if (i != size - 1) toValueArr += ", ";
}
else if (valueType[0] == 'd')
for (int i = 0; i < size; ++i) {
toValueArr += std::to_string(fromValueArr[i]);
if (i != size - 1) toValueArr += ", ";
}
_ini.SetValue(section.c_str(), key.c_str(), toValueArr.c_str());
}
void save() {
std::string output;
_ini.Save(output);
_ini.SaveFile(path().c_str());
}
};
#endif // INIFILE_HPP_
*.ini
文件是Initialization file
的缩写,即为初始化文件,是 Windows 系统配置文件所采用的存储格式,统管 Windows 的各项配置。
用的一开源库 simpleini,再根据实际业务需求封了一层功能函数,能对 ini 文件做读取 getFromIni
和写入 setToIni
操作,用函数重载即可对不同的参数类型做处理。
JsonFile
#ifndef JSONFILE_HPP_
#define JSONFILE_HPP_
#include "UFile.hpp"
#include "json/json.hpp"
using json = nlohmann::json;
class JsonFile : public UFile {
public:
json _data;
explicit JsonFile(const std::string &path) : UFile(path) {}
explicit JsonFile(const fs::path &path) : UFile(path) {}
~JsonFile() {}
bool setup() {
std::ifstream file(path());
if (!file) {
return false;
}
file >> _data;
file.close();
return true;
}
void getFromJson(const std::string &key, std::string ¶m,
const std::string &defaultVal) {
if (key == "") {
param = defaultVal;
return;
}
json temp = _data;
std::vector<std::string> keyArr = split(key, ".");
for (int i = 0; i < keyArr.size() - 1; ++i)
if (temp.contains(keyArr[i])) temp = temp.at(keyArr[i]);
if (temp.contains(keyArr[keyArr.size() - 1]))
param = temp.at(keyArr[keyArr.size() - 1]);
else
param = defaultVal;
}
template <typename T>
void getFromJson(const std::string &key, T ¶m, T defaultVal) {
if (key == "") {
param = defaultVal;
return;
}
json temp = _data;
std::vector<std::string> keyArr = split(key, ".");
for (int i = 0; i < keyArr.size() - 1; ++i)
if (temp.contains(keyArr[i])) temp = temp.at(keyArr[i]);
if (temp.contains(keyArr[keyArr.size() - 1]))
param = temp.at(keyArr[keyArr.size() - 1]);
else
param = defaultVal;
}
void getFromJson(const std::string &key, std::string *param,
const std::string *defaultVal, const int &size) {
int index = 0;
if (key == "") {
for (int i = index; i < size; ++i) param[i] = defaultVal[i];
return;
}
json temp = _data;
std::vector<std::string> keyArr = split(key, ".");
for (int i = 0; i < keyArr.size() - 1; ++i)
if (temp.contains(keyArr[i])) temp = temp.at(keyArr[i]);
if (temp.contains(keyArr[keyArr.size() - 1])) {
const json thisKeyArrValue = temp.at(keyArr[keyArr.size() - 1]);
for (int i = 0; i < thisKeyArrValue.size(); ++i)
param[index++] = thisKeyArrValue[index];
}
if (index < size)
for (int i = index; i < size; ++i) param[i] = defaultVal[i];
}
template <typename T>
void getFromJson(const std::string &key, T *param, const T *defaultVal,
const int &size) {
int index = 0;
if (key == "") {
for (int i = index; i < size; ++i) param[i] = defaultVal[i];
return;
}
json temp = _data;
std::vector<std::string> keyArr = split(key, ".");
for (int i = 0; i < keyArr.size() - 1; ++i)
if (temp.contains(keyArr[i])) temp = temp.at(keyArr[i]);
if (temp.contains(keyArr[keyArr.size() - 1])) {
const json thisKeyArrValue = temp.at(keyArr[keyArr.size() - 1]);
for (int i = 0; i < thisKeyArrValue.size(); ++i)
param[index++] = thisKeyArrValue[index];
}
if (index < size)
for (int i = index; i < size; ++i) param[i] = defaultVal[i];
}
void save() {
std::ofstream file(path());
file << _data;
file.flush();
}
};
#endif // JSONFILE_HPP_
也用的一开源库 nlohmann/json,和 IniFile 一样做法啦。
BmpFile
#ifndef BMPFILE_HPP_
#define BMPFILE_HPP_
#include "UFile.hpp"
#include "bmp/BMP.h"
class BmpFile : public UFile {
public:
explicit BmpFile(const std::string& path) : UFile(path) {}
explicit BmpFile(const fs::path& path) : UFile(path) {}
~BmpFile() {}
};
#endif // BMPFILE_HPP_
呃,也用的开源库,是公司的前辈 clone 的,我没找到来源,不过还是日常感谢无私的开源贡献者。
百度百科 - BMP 是英文 Bitmap(位图)的简写,它是 Windows 操作系统中的标准图像文件格式,能够被多种 Windows 应用程序所支持。随着 Windows 操作系统的流行与丰富的 Windows 应用程序的开发,BMP 位图格式理所当然地被广泛应用。这种格式的特点是包含的图像信息较丰富,几乎不进行压缩,但由此导致了它与生俱来的缺点--占用磁盘空间过大。所以,BMP 在单机上比较流行。
这个开源库的接口考虑的很全面了,处理目前的业务够用了,就没做二次封装,不包了,直接用,仅仅是继承于 UFile
,便于做路径处理。
BinFile
主要用于二进制文件的处理(读取、写入、追加),和 TxtFile
一样,使用的都是 std::ifstream
和 std::ofstream
,打开选项多了个 std::ios::binary
,用于指定以二进制方式打开文件,以及需要用到 seekg
来定位文件指针,计算出文件的长度。
#ifndef BINFILE_HPP_
#define BINFILE_HPP_
#include <fstream>
#include <vector>
#include "UFile.hpp"
class BinFile : public UFile {
private:
std::vector<uint8_t> _data;
int _length = 0;
public:
explicit BinFile(const std::string &path) : UFile(path) {}
explicit BinFile(const fs::path &path) : UFile(path) {}
void setData(const std::vector<uint8_t> &data) { _data = data; }
std::vector<uint8_t> getData() const { return _data; }
void setLength(const int &length) { _length = length; }
int getLength() const { return _length; }
bool readData() {
std::ifstream file(path(), std::ios::in | std::ios::binary);
if (!file) {
return false;
}
file.seekg(0, std::ios::end);
_length = (int)file.tellg();
file.seekg(0, std::ios::beg);
_data.resize(_length);
file.read((char *)_data.data(), _length);
file.close();
return true;
}
bool writeData() {
std::ofstream file(path(), std::ios::out | std::ios::binary);
if (!file) {
return false;
}
file.write((const char *)_data.data(), _length);
file.close();
return true;
}
bool appendWriteData(std::vector<uint8_t> data) {
std::ofstream file(path(),
std::ios::out | std::ios::binary | std::ios::app);
if (!file) {
return false;
}
_data.insert(_data.end(), data.begin(), data.end());
_length = (int)_data.size();
file.write((const char *)_data.data(), _length);
file.close();
return true;
}
};
#endif // BINFILE_HPP_
用来处理以下两种数据:
- dat
.dat
是二进制文件;
- img
处理图像文件其实也是对二进制的处理;
小造轮子
UString
#ifndef USTRING_HPP_
#define USTRING_HPP_
#include <regex>
#include <string>
#include <vector>
class UString {
private:
std::string _content;
public:
UString() : _content("") {}
explicit UString(const std::string &content) : _content(content) {}
~UString() {}
UString args(const std::string &substitution) {
std::string upToDateContent = "";
std::string suffix = "";
std::regex percentSign("%([1-9]{1})");
auto content_begin =
std::sregex_iterator(_content.begin(), _content.end(), percentSign);
auto content_end = std::sregex_iterator();
for (std::sregex_iterator i = content_begin; i != content_end; ++i) {
std::smatch match = *i;
std::string match_str = match.str();
int index = match_str[1] - '1';
if (index == 0)
upToDateContent += match.prefix().str() + substitution;
else
upToDateContent += match.prefix().str() + "%" + std::to_string(index);
suffix = match.suffix();
}
upToDateContent += suffix;
return UString(upToDateContent);
}
UString args(const char *substitution) {
return args(std::string(substitution));
}
template <typename T>
UString args(T substitution) {
return args(std::to_string(substitution));
}
template <class T, class... Args>
UString args(T head, Args... rest) {
UString result(_content);
result = result.args(head);
result = result.args(rest...);
return result;
}
operator std::string() { return _content; }
const char *c_str() { return _content.c_str(); }
friend std::ostream &operator<<(std::ostream &os, const UString &uString) {
os << uString._content;
return os;
}
};
#endif // USTRING_HPP_
其实就是 QString 有一个 arg
的方法(用于字符串的构建)太好用了,软件组舍不得,想保留这个方法。
不过 C++ 标准的 std::string
没有类似的功能函数,那我就浅浅造个小轮子。
造起来不难,小小难点就三个:
-
正则表达式库
<regex>
的使用,用于字符串的替换; -
链式调用,那就函数重载;
-
传递多个参数,模板接受,然后递归即可,会自动识别对应的同名函数做处理;
要重载一下 <<
操作符,否则 std::ostream
输出流识别不了 UString
这个类。
还有一个类型重载,这样就能用 =
操作符,将右值(Ustring
对象)重载为 std::string
。
借轮子
Variant
用于替代 Qt 中的 QVariant
,处理不同数据类型之间的转换。
前辈在 CSDN 上找的,软件组的大家在实际使用中,根据业务的需求,有做一些修改和 bug 修复。
这个代码太长了,不展示出来,看这里