Compare commits

...

2 Commits

Author SHA1 Message Date
2206825223 doc: add document for the change of loading function of ConfigManager. 2024-11-02 17:31:19 +08:00
21f7e7f786 feat: add new helper for scoped enum type.
- Add EnumHelper for bitwise operation of scoped enum type (copied from libcmo21)
- Enrich the return value of ConfigManager load function to present more infomation of loading.
- update testbench for new added feature and modification.
- add document for new added feature.
2024-11-02 17:10:55 +08:00
9 changed files with 386 additions and 29 deletions

View File

@ -86,12 +86,66 @@ so it must be initialized after initializing all settings.
When initializing core manager, you need assign config file path first.
Then you need specify a version number.
Version number will be used when reading config file.
If the version of config file is higher than your given number,
core manager will assume you are trying to read a config file created by a higher version program.
Core manager will reject reading and use default value for all settings.
Otherwise, core manager will try to read config file and do proper migration if possible.
Version number is important.
It will be used when reading config file and only can be increased if needed (version can not downgrade).
The last argument is an initializer list which contain the \b pointer to all settings this manager managed.
When executing YYCC::ConfigManager::CoreManager::Load to load configs, it will perform following steps one by one:
<UL>
<LI>
Open given config file.
<UL>
<LI>
If given file is not existing, loading function will simply return and all configs will be reset to its default value.
</LI>
<LI>
Success to open file, go to next step.
</LI>
</UL>
</LI>
<LI>
Fetch version number from file.
<UL>
<LI>
If fail to read version number from file, loading function will simply return and all configs will be reset to its default value.
</LI>
<LI>
If the version of config file is higher than your specified version number when constructing this class,
core manager will assume you are trying to read a config file created by a higher version program,
and will reject reading and use default value for all settings.
</LI>
<LI>
If the version of config file is lower than your specified version number,
core manager will try to read config file and do proper migration (set default value for configs which do not existing) if possible.
</LI>
<LI>
If the version of config file is equal than your specified version number,
core manager will read config file normally.
</LI>
</UL>
</LI>
<LI>
Read config file body.
<UL>
<LI>
If any IO error occurs when reading, loading function will simply return.
All read config will keep their read value and all configs which has not been read will keep their default value.
</LI>
<LI>
If some config can not parse binary data to its type,
this config will be skipped and core manager will process next config.
This config will keep its default value.
</LI>
</UL>
</LI>
</UL>
All of these scenarios can be found by the return value of loading function.
The return type of loading function, ConfigLoadResult is a flag enum.
You can find whether loading process happend specified issue by using bitwise operation on it.
*/
}

35
doc/src/enum_helper.dox Normal file
View File

@ -0,0 +1,35 @@
namespace YYCC::EnumHelper {
/**
\page enum_helper Scoped Enum Helper
\section enum_helper__intro Intro
C++ introduce a new enum called scoped enum.
It is better than legacy C enum because it will not leak name into namespace where it locate,
and also can specify an underlying type to it to make sure it is stored as specified size.
However, the shortcoming of it is that it lack bitwise operator comparing with legacy C enum.
Programmer must implement them for scoped enum one by one.
It is a hardship and inconvenient.
This is the reason why I invent this class
\section enum_helper__Usage Usage
In this namespace, we provide all bitwise functions related to scoped enum type which may be used.
See YYCC::EnumHelper for more detail (It is more clear to read function annotation than I introduce in there repeatedly).
\section enum_helper__why Why not Operator Overload
I have try it (and you even can see the relic of it in source code).
But it need a extra statement written in following to include it, otherwise compiler can not see it.
\code
using namespace YYCC::EnumHelper;
\endcode
Another reason why I do not use this method is that
this overload strategy may be applied to some type which should not be applied by accient, such as non-scoped enum type.
So I gave up this solution.
*/
}

View File

@ -45,6 +45,8 @@
\li \subpage std_patch
\li \subpage enum_helper
<B>Advanced Features</B>
\li \subpage constraints

View File

@ -31,6 +31,7 @@ FILES
ConsoleHelper.hpp
DialogHelper.hpp
EncodingHelper.hpp
EnumHelper.hpp
ExceptionHelper.hpp
StdPatch.hpp
IOHelper.hpp

View File

@ -2,6 +2,7 @@
#include "EncodingHelper.hpp"
#include "IOHelper.hpp"
#include "EnumHelper.hpp"
#include <stdexcept>
namespace YYCC::ConfigManager {
@ -33,7 +34,7 @@ namespace YYCC::ConfigManager {
m_CfgFilePath(cfg_file_path), m_VersionIdentifier(version_identifier), m_Settings() {
// Mark: no need to check cfg file path
// it will be checked at creating file handle
// assign settings
for (auto* setting : settings) {
auto result = m_Settings.try_emplace(setting->GetName(), setting);
@ -44,7 +45,10 @@ namespace YYCC::ConfigManager {
}
}
bool CoreManager::Load() {
ConfigLoadResult CoreManager::Load() {
// prepare result variables
ConfigLoadResult ret = ConfigLoadResult::OK;
// reset all settings first
Reset();
@ -53,20 +57,27 @@ namespace YYCC::ConfigManager {
if (fs.get() == nullptr) {
// if we fail to get, it means that we do not have corresponding cfg file.
// all settings should be reset to default value.
return true;
ret = ConfigLoadResult::Created;
return ret;
}
// fetch version info
uint64_t version_info;
if (std::fread(&version_info, 1u, sizeof(version_info), fs.get()) != sizeof(version_info))
return false;
if (std::fread(&version_info, 1u, sizeof(version_info), fs.get()) != sizeof(version_info)) {
ret = ConfigLoadResult::Created;
return ret;
}
// check version
// if read version is greater than we expected,
// it means that this cfg file is created by the program higer than this.
// we should not read anything from it.
// however, for compaitibility reason, we allow read old cfg data.
if (version_info > m_VersionIdentifier)
return true;
if (version_info > m_VersionIdentifier) {
ret = ConfigLoadResult::ForwardNew;
return ret;
} else if (version_info < m_VersionIdentifier) {
EnumHelper::Add(ret, ConfigLoadResult::Migrated);
}
// fetch setting item from file
yycc_u8string name_cache;
@ -77,37 +88,50 @@ namespace YYCC::ConfigManager {
if (std::fread(&name_length, 1u, sizeof(name_length), fs.get()) != sizeof(name_length)) {
// we also check whether reach EOF at there.
if (std::feof(fs.get())) break;
else return false;
else {
EnumHelper::Add(ret, ConfigLoadResult::BrokenFile);
return ret;
}
}
// fetch name body
name_cache.resize(name_length);
if (std::fread(name_cache.data(), 1u, name_length, fs.get()) != name_length)
return false;
if (std::fread(name_cache.data(), 1u, name_length, fs.get()) != name_length) {
EnumHelper::Add(ret, ConfigLoadResult::BrokenFile);
return ret;
}
// get setting data length
size_t data_length;
if (std::fread(&data_length, 1u, sizeof(data_length), fs.get()) != sizeof(data_length))
return false;
if (std::fread(&data_length, 1u, sizeof(data_length), fs.get()) != sizeof(data_length)) {
EnumHelper::Add(ret, ConfigLoadResult::BrokenFile);
return ret;
}
// get matched setting first
const auto& found = m_Settings.find(name_cache);
if (found != m_Settings.end()) {
// found. read data for it
found->second->ResizeData(data_length);
if (std::fread(found->second->GetDataPtr(), 1u, data_length, fs.get()) != data_length)
return false;
if (std::fread(found->second->GetDataPtr(), 1u, data_length, fs.get()) != data_length) {
EnumHelper::Add(ret, ConfigLoadResult::BrokenFile);
return ret;
}
// call user defined load function
// if fail to parse, reset to default value
if (!found->second->UserLoad())
if (!found->second->UserLoad()) {
EnumHelper::Add(ret, ConfigLoadResult::ItemError);
found->second->UserReset();
}
} else {
// fail to find. skip this unknown setting
if (fseek(fs.get(), static_cast<long>(data_length), SEEK_CUR) != 0)
return false;
if (fseek(fs.get(), static_cast<long>(data_length), SEEK_CUR) != 0) {
EnumHelper::Add(ret, ConfigLoadResult::BrokenFile);
return ret;
}
}
}
return true;
return ret;
}
bool CoreManager::Save() {

View File

@ -17,6 +17,19 @@
* @details For how to use this namespace, please see \ref config_manager.
*/
namespace YYCC::ConfigManager {
/**
* @brief The load result of loading config.
*/
enum class ConfigLoadResult {
OK = 0, ///< Success load configs.
Created = 1 << 0, ///< Given file is not existing, we create all configs in default values.
ForwardNew = 1 << 1, ///< Detect the config file created by higher version. We create all configs in default values.
Migrated = 1 << 2, ///< Detect the config file created by lower version. We try migrate configs written in it.
BrokenFile = 1 << 3, ///< Given file has bad format. Thus some configs are kept as its default values.
ItemError = 1 << 4 ///< Some config can not be recognized from the data read from file so they are reset to default value.
};
using UnderlyingConfigLoadResult_t = std::underlying_type_t<ConfigLoadResult>;
/// @brief The base class of every setting.
/// @details Programmer can inherit this class and implement essential functions to create custom setting.
@ -75,7 +88,7 @@ namespace YYCC::ConfigManager {
private:
std::vector<uint8_t> m_RawData;
};
/// @brief Settings manager and config file reader writer.
class CoreManager {
public:
@ -96,8 +109,8 @@ namespace YYCC::ConfigManager {
public:
/// @brief Load settings from file.
/// @details Before loading, all settings will be reset to default value first.
/// @return True if success, otherwise false.
bool Load();
/// @return What happend when loading config. This function always success.
ConfigLoadResult Load();
/// @brief Save settings to file.
/// @return True if success, otherwise false.
bool Save();

177
src/EnumHelper.hpp Normal file
View File

@ -0,0 +1,177 @@
#pragma once
#include "YYCCInternal.hpp"
#include <initializer_list>
#include <type_traits>
/**
* @brief The namespace for convenient C++ enum class logic operations.
* @details
* C++ enum class statement is a modern way to declare enum in C++.
* But it lack essential logic operations which is commonly used by programmer.
* So we create this helper to resolve this issue.
*/
namespace YYCC::EnumHelper {
//// Reference:
//// Enum operator overload: https://stackoverflow.com/a/71107019
//// Constexpr operator overload: https://stackoverflow.com/a/17746099
//template<typename TEnum, std::enable_if_t<std::is_enum_v<TEnum>, int> = 0>
//inline constexpr TEnum operator|(TEnum lhs, TEnum rhs) {
// using ut = std::underlying_type_t<TEnum>;
// return static_cast<TEnum>(static_cast<ut>(lhs) | static_cast<ut>(rhs));
//}
//template<typename TEnum, std::enable_if_t<std::is_enum_v<TEnum>, int> = 0>
//inline constexpr TEnum operator|=(TEnum& lhs, TEnum rhs) {
// using ut = std::underlying_type_t<TEnum>;
// lhs = lhs | rhs;
// return lhs;
//}
//
//template<typename TEnum, std::enable_if_t<std::is_enum_v<TEnum>, int> = 0>
//inline constexpr TEnum operator&(TEnum lhs, TEnum rhs) {
// using ut = std::underlying_type_t<TEnum>;
// return static_cast<TEnum>(static_cast<ut>(lhs) & static_cast<ut>(rhs));
//}
//template<typename TEnum, std::enable_if_t<std::is_enum_v<TEnum>, int> = 0>
//inline constexpr TEnum operator&=(TEnum& lhs, TEnum rhs) {
// using ut = std::underlying_type_t<TEnum>;
// lhs = lhs & rhs;
// return lhs;
//}
//
//template<typename TEnum, std::enable_if_t<std::is_enum_v<TEnum>, int> = 0>
//inline constexpr TEnum operator^(TEnum lhs, TEnum rhs) {
// using ut = std::underlying_type_t<TEnum>;
// return static_cast<TEnum>(static_cast<ut>(lhs) ^ static_cast<ut>(rhs));
//}
//template<typename TEnum, std::enable_if_t<std::is_enum_v<TEnum>, int> = 0>
//inline constexpr TEnum operator^=(TEnum& lhs, TEnum rhs) {
// using ut = std::underlying_type_t<TEnum>;
// lhs = lhs ^ rhs;
// return lhs;
//}
//template<typename TEnum, std::enable_if_t<std::is_enum_v<TEnum>, int> = 0>
//inline constexpr TEnum operator~(TEnum lhs) {
// using ut = std::underlying_type_t<TEnum>;
// return static_cast<TEnum>(~(static_cast<ut>(lhs)));
//}
//
//template<typename TEnum, std::enable_if_t<std::is_enum_v<TEnum>, int> = 0>
//inline constexpr bool operator bool(TEnum lhs) {
// using ut = std::underlying_type_t<TEnum>;
// return static_cast<bool>(static_cast<ut>(lhs));
//}
/**
* @brief The helper struct to check all given template argument are the same enum type.
* @tparam TEnum The template parameter to be checked (first one).
* @tparam Ts The template parameter to be checked.
*/
template<typename TEnum, typename... Ts>
struct all_enum_values {
public:
// Please note it is std::is_same, not std::is_same_v!
// That's std::conjunction_v required.
static constexpr bool value = std::is_enum_v<std::remove_cv_t<TEnum>> && std::conjunction_v<std::is_same<std::remove_cv_t<TEnum>, std::remove_cv_t<Ts>>...>;
};
/**
* @brief The convenient calling to all_enum_values::value to check enum template parameter.
* @tparam TEnum The template parameter to be checked (first one).
* @tparam Ts The template parameter to be checked.
*/
template<typename TEnum, typename... Ts>
inline constexpr bool all_enum_values_v = all_enum_values<TEnum, Ts...>::value;
/**
* @brief Merge given enum flags like performing <TT>e1 | e2 | ... | en</TT>
* @tparam TEnum Enum type for processing.
* @param[in] il The list of enum flags to be merged.
* @return The merged enum flag.
*/
template<typename TEnum, typename... Ts, std::enable_if_t<all_enum_values_v<TEnum, Ts...>, int> = 0>
constexpr TEnum Merge(TEnum val, Ts... val_left) {
using ut = std::underlying_type_t<TEnum>;
ut result = static_cast<ut>(val);
if constexpr (sizeof...(val_left) > 0) {
result |= static_cast<ut>(Merge(val_left...));
}
return static_cast<TEnum>(result);
}
/**
* @brief Reverse given enum flags like performing <TT>~(e)</TT>
* @tparam TEnum Enum type for processing.
* @param[in] e The list of enum flags to be inversed.
* @return The inversed enum flag.
*/
template<typename TEnum, std::enable_if_t<std::is_enum_v<TEnum>, int> = 0>
constexpr TEnum Invert(TEnum e) {
using ut = std::underlying_type_t<TEnum>;
return static_cast<TEnum>(~(static_cast<ut>(e)));
}
/**
* @brief Use specified enum flags to mask given enum flags like performing <TT>e1 &= e2</TT>
* @tparam TEnum Enum type for processing.
* @param[in,out] e1 The enum flags to be masked.
* @param[in] e2 The mask enum flag.
*/
template<typename TEnum, std::enable_if_t<std::is_enum_v<TEnum>, int> = 0>
constexpr void Mask(TEnum& e1, TEnum e2) {
using ut = std::underlying_type_t<TEnum>;
e1 = static_cast<TEnum>(static_cast<ut>(e1) & static_cast<ut>(e2));
}
/**
* @brief Add specified enum flags to given enum flags like performing <TT>e1 = e1 | e2 | ... | en</TT>
* @tparam TEnum Enum type for processing.
* @param[in,out] e1 The enum flags to be processed.
* @param[in] e2 The enum flag to be added.
*/
template<typename TEnum, typename... Ts, std::enable_if_t<all_enum_values_v<TEnum, Ts...>, int> = 0>
constexpr void Add(TEnum& e1, Ts... vals) {
using ut = std::underlying_type_t<TEnum>;
e1 = static_cast<TEnum>(static_cast<ut>(e1) | static_cast<ut>(Merge(vals...)));
}
/**
* @brief Remove specified enum flags from given enum flags like performing <TT>e1 &= ~(e2 | e3 | ... | en)</TT>
* @tparam TEnum Enum type for processing.
* @param[in,out] e1 The enum flags to be processed.
* @param[in] e2 The enum flag to be removed.
*/
template<typename TEnum, typename... Ts, std::enable_if_t<all_enum_values_v<TEnum>, int> = 0>
constexpr void Remove(TEnum& e1, Ts... vals) {
using ut = std::underlying_type_t<TEnum>;
e1 = static_cast<TEnum>(static_cast<ut>(e1) & static_cast<ut>(Invert(Merge(vals...))));
}
/**
* @brief Check whether given enum flags has specified enum flag like performing <TT>bool(e & probe)</TT>
* @tparam TEnum Enum type for processing.
* @param[in] e1 The enum flags to be checked.
* @param[in] e2 The enum flag for checking.
* @return True if it has, otherwise false.
*/
template<typename TEnum, std::enable_if_t<std::is_enum_v<TEnum>, int> = 0>
constexpr bool Has(TEnum e1, TEnum e2) {
using ut = std::underlying_type_t<TEnum>;
return static_cast<bool>(static_cast<ut>(e1) & static_cast<ut>(e2));
}
/**
* @brief Cast given enum flags to its equvalent boolean value like performing <TT>bool(e)</TT>
* @tparam TEnum Enum type for processing.
* @param e The enum flags to be cast.
* @return The cast enum flag.
*/
template<typename TEnum, std::enable_if_t<std::is_enum_v<TEnum>, int> = 0>
constexpr bool Bool(TEnum e) {
using ut = std::underlying_type_t<TEnum>;
return static_cast<bool>(static_cast<ut>(e));
}
}

View File

@ -11,6 +11,7 @@
#include "IOHelper.hpp"
#include "WinFctHelper.hpp"
#include "StdPatch.hpp"
#include "EnumHelper.hpp"
#include "ExceptionHelper.hpp"
#include "ConfigManager.hpp"

View File

@ -402,7 +402,7 @@ namespace YYCCTestbench {
#endif
}
static void StdPatch() {
static void StdPatchTestbench() {
// Std Path
@ -439,6 +439,51 @@ namespace YYCCTestbench {
}
enum class TestFlagEnum : uint8_t {
Test1 = 0b00000000,
Test2 = 0b00000001,
Test3 = 0b00000010,
Test4 = 0b00000100,
Test5 = 0b00001000,
Test6 = 0b00010000,
Test7 = 0b00100000,
Test8 = 0b01000000,
Test9 = 0b10000000,
Inverted = 0b01111111,
Merged = Test3 + Test5,
};
static void EnumHelperTestbench() {
TestFlagEnum val;
Assert(YYCC::EnumHelper::Merge(TestFlagEnum::Test3, TestFlagEnum::Test5) == TestFlagEnum::Merged, YYCC_U8("YYCC::EnumHelper::Merge"));
Assert(YYCC::EnumHelper::Invert(TestFlagEnum::Test9) == TestFlagEnum::Inverted, YYCC_U8("YYCC::EnumHelper::Invert"));
val = YYCC::EnumHelper::Merge(TestFlagEnum::Test3, TestFlagEnum::Test5);
YYCC::EnumHelper::Mask(val, TestFlagEnum::Test3);
Assert(YYCC::EnumHelper::Bool(val), YYCC_U8("YYCC::EnumHelper::Mask"));
val = YYCC::EnumHelper::Merge(TestFlagEnum::Test3, TestFlagEnum::Test5);
YYCC::EnumHelper::Mask(val, TestFlagEnum::Test4);
Assert(!YYCC::EnumHelper::Bool(val), YYCC_U8("YYCC::EnumHelper::Mask"));
val = TestFlagEnum::Test3;
YYCC::EnumHelper::Add(val, TestFlagEnum::Test5);
Assert(val == TestFlagEnum::Merged, YYCC_U8("YYCC::EnumHelper::Add"));
val = TestFlagEnum::Merged;
YYCC::EnumHelper::Remove(val, TestFlagEnum::Test5);
Assert(val == TestFlagEnum::Test3, YYCC_U8("YYCC::EnumHelper::Remove"));
val = YYCC::EnumHelper::Merge(TestFlagEnum::Test3, TestFlagEnum::Test5);
Assert(YYCC::EnumHelper::Has(val, TestFlagEnum::Test3), YYCC_U8("YYCC::EnumHelper::Has"));
Assert(!YYCC::EnumHelper::Has(val, TestFlagEnum::Test4), YYCC_U8("YYCC::EnumHelper::Has"));
Assert(!YYCC::EnumHelper::Bool(TestFlagEnum::Test1), YYCC_U8("YYCC::EnumHelper::Bool"));
Assert(YYCC::EnumHelper::Bool(TestFlagEnum::Test2), YYCC_U8("YYCC::EnumHelper::Bool"));
}
enum class TestEnum : int8_t {
Test1, Test2, Test3
};
@ -518,7 +563,11 @@ namespace YYCCTestbench {
Assert(test.m_EnumSetting.Get() == TestEnum::Test1, YYCC_U8("YYCC::ConfigManager::CoreManager::Reset"));
// test load
Assert(test.m_CoreManager.Load(), YYCC_U8("YYCC::ConfigManager::CoreManager::Load"));
YYCC::ConfigManager::ConfigLoadResult wrong_result = YYCC::EnumHelper::Merge(
YYCC::ConfigManager::ConfigLoadResult::ItemError,
YYCC::ConfigManager::ConfigLoadResult::BrokenFile
);
Assert(!YYCC::EnumHelper::Has(test.m_CoreManager.Load(), wrong_result), YYCC_U8("YYCC::ConfigManager::CoreManager::Load"));
test.PrintSettings();
Assert(test.m_IntSetting.Get() == INT32_C(114), YYCC_U8("YYCC::ConfigManager::CoreManager::Load"));
Assert(test.m_FloatSetting.Get() == 2.0f, YYCC_U8("YYCC::ConfigManager::CoreManager::Load"));
@ -655,7 +704,8 @@ int main(int argc, char* argv[]) {
YYCCTestbench::StringTestbench();
YYCCTestbench::ParserTestbench();
YYCCTestbench::WinFctTestbench();
YYCCTestbench::StdPatch();
YYCCTestbench::StdPatchTestbench();
YYCCTestbench::EnumHelperTestbench();
// advanced
YYCCTestbench::ConfigManagerTestbench();
YYCCTestbench::ArgParserTestbench(argc, argv);