Please take a review of my simple .ini config loader / saver:
//
// value wrapper
//
class IniEntry {
public:
//
// default ctor
IniEntry() {
}
//
// copy ctor
IniEntry(const IniEntry& rhs) : _Data(rhs._Data) {
}
//
// move ctor
IniEntry(IniEntry&& rhs) noexcept : _Data(std::move(rhs._Data)) {
}
//
// dtor
~IniEntry() {
}
public:
//
// copy assignment
IniEntry& operator=(const IniEntry& rhs) {
_Data = rhs._Data;
return *this;
}
//
// move assignment
IniEntry& operator=(IniEntry&& rhs) noexcept {
_Data = std::move(rhs._Data);
return *this;
}
//
// string assignment
IniEntry& operator=(const char* value) {
SetAsString(value);
return *this;
}
//
// integer assignment
IniEntry& operator=(int value) {
SetAsInteger(value);
return *this;
}
//
// float assignment
IniEntry& operator=(float value) {
SetAsFloat(value);
return *this;
}
public:
//
// string explicit cast
explicit operator const char*() const {
return GetAsString();
}
//
// integer explicit cast
explicit operator int() const {
return GetAsInteger();
}
//
// float explicit cast
explicit operator float() const {
return GetAsFloat();
}
public:
//
// set data from string
inline void SetAsString(const char* value) {
_Data.assign(value);
}
//
// get data as string
inline const char* GetAsString(const char* default_return = "") const {
return _Data.empty() ? default_return : _Data.c_str();
}
//
// set data from integer
inline void SetAsInteger(int value) {
_Data.assign(std::to_string(value));
}
//
// get data as integer
inline int GetAsInteger(int default_return = 0) const {
try {
return std::stoi(_Data);
}
catch (...) {
return default_return;
}
}
//
// set data from float
inline void SetAsFloat(float value) {
_Data.assign(std::to_string(value));
}
//
// get data as float
inline float GetAsFloat(float default_return = 0.f) const {
try {
return std::stof(_Data);
}
catch (...) {
return default_return;
}
}
public:
//
// check if data empty
bool Empty() const {
return _Data.empty();
}
private:
std::string _Data;
};
//
// util object to restrict copy (and move if not declared move information)
//
class IniNonCopyable {
public:
IniNonCopyable(const IniNonCopyable&) = delete;
IniNonCopyable& operator=(const IniNonCopyable&) = delete;
};
//
// container controller base
//
template<class Element_t>
class IniContainer : public IniNonCopyable {
typedef std::shared_ptr<Element_t> ContainerElement_t;
typedef std::string ContainerKey_t;
typedef std::unordered_map<ContainerKey_t, ContainerElement_t> Container_t;
public:
//
// default ctor
IniContainer() {
}
//
// dtor
virtual ~IniContainer() {
}
public:
//
// access to container element
// if key does not exist - will be created new one
Element_t& operator[](const std::string& key) {
auto it = _Container.find(key);
if (it != _Container.end()) {
return *it->second.get();
}
auto instance = new Element_t;
_Container.insert(std::make_pair(key, instance));
return *instance;
}
//
// check if container empty
bool operator!() const {
return Empty();
}
public:
//
// create new element
// if key already exist will be returned empty element
virtual Element_t& Create(const std::string& key) {
if (Has(key)) {
static Element_t empty;
return empty;
}
auto instance = new Element_t;
_Container.insert(std::make_pair(key, instance));
return *instance;
}
//
// delete existed element
virtual bool Delete(const std::string& key) {
auto it = _Container.find(key);
if (it == _Container.end()) {
return false;
}
_Container.erase(key);
return true;
}
//
// access to container element
// if key does not exist - will be returned empty element
virtual Element_t& At(const std::string& key) const {
auto it = _Container.find(key);
if (it == _Container.end()) {
static Element_t empty;
return empty;
}
return *it->second.get();
}
//
// check if element at key exist
virtual bool Has(const std::string& key) const {
return _Container.find(key) != _Container.end();
}
//
// container clear
virtual void Clear() {
_Container.clear();
}
//
// check if container empty
virtual bool Empty() const {
return _Container.empty();
}
public:
//
// return ref to container data
virtual const Container_t& GetContainer() const {
return _Container;
}
protected:
Container_t _Container;
};
//
// section handler (IniEntry container)
//
class IniSection : public IniContainer<IniEntry> {
public:
//
// default ctor
IniSection() {
}
//
// dtor
virtual ~IniSection() {
}
};
//
// file handler (IniSection container)
//
class IniFile : public IniContainer<IniSection> {
public:
//
// default ctor
// if path not empty then loading will be started automatically
explicit IniFile(const std::string& path = "") : _Path(path) {
if (!_Path.empty()) {
Load();
}
}
//
// dtor
virtual ~IniFile() {
}
public:
//
// file parsing
// if path empty, then will be used ctor initialized path
bool Load(const std::string& path = "") {
const std::string file_path(path.empty() ? _Path : path);
if (file_path.empty()) {
return false;
}
std::ifstream file_stream(file_path);
if (!file_stream) {
return false;
}
const std::regex comment_regex(R"x(\s*[;#])x");
const std::regex section_regex(R"x(\s*\[([^\]]+)\])x");
const std::regex value_regex(R"x(\s*(\S[^ \t=]*)\s*=\s*((\s?\S+)+)\s*$)x");
std::string file_line;
std::smatch result;
std::string section;
while (std::getline(file_stream, file_line)) {
if (file_line.empty() || std::regex_match(file_line, result, comment_regex)) {
}
else if (std::regex_match(file_line, result, section_regex) && result.size() == 2) {
section.assign(result[1]);
Create(section);
}
else if (std::regex_match(file_line, result, value_regex) && result.size() == 4) {
At(section)[result[1]] = result[2].str().c_str();
}
}
return true;
}
//
// file saving
// if path empty, then will be used ctor initialized path
bool Save(const std::string& path = "") {
const std::string file_path(path.empty() ? _Path : path);
if (file_path.empty()) {
return false;
}
std::ofstream file_stream(file_path);
if (!file_stream) {
return false;
}
for (auto& section : GetContainer()) {
file_stream << "[" << section.first << "]" << std::endl;
for (auto& entry : section.second->GetContainer()) {
file_stream << entry.first << " = " << (const char*)*entry.second << std::endl;
}
file_stream << std::endl;
}
return true;
}
private:
std::string _Path;
};
The code has declaration / implementation in the same file, just because it's not finished yet. In the future, of course, those will be separated.
Example of usage:
int main() {
IniFile file("Test.ini");
if (!file) {
std::cout << "failed to load" << std::endl;
return 0;
}
auto& general = file["General"];
if (!general) {
std::cout << "failed to pick section" << std::endl;
return 0;
}
auto title = general["Title"].GetAsString("Example");
auto rate = general["Rate"].GetAsInteger(100);
auto scale = general["Scale"].GetAsFloat(200.f);
// update and save example
general["Rate"] = (int)general["Rate"] + 1;
if (!file.Save()) {
std::cout << "failed to save" << std::endl;
return 0;
}
}