I've written a modern C++17 INI file reader to get access parameters values from an INI configuration file anywhere in a source file by simply included #include "Ini.hpp"
in the headers.
The file parsing is done once at first parameter request with a default file path. The parsing is done with REGEX I've written on this purpose.
I may went too far when checking the different number ranges depending on the C++ number type (short, int, long, long long, ...
).
Let me know what you think.
Ini.hpp
#include <fstream>
#include <unordered_map>
#include <string>
#include <regex>
#include <cfloat>
#include <climits>
// PARAMETERS_INI_FILE_PATH is a constant defined in the CMake configuration project file
struct Ini
{
public:
typedef std::unordered_map<std::string, std::unordered_map<std::string, std::string>> IniStructure;
Ini() = delete;
static void parse(const std::string &filePath = PARAMETERS_INI_FILE_PATH);
template<typename T> static T get(const std::string §ion, const std::string &key);
private:
static bool keyExists(const std::string §ion, const std::string &key);
static long long extractIntegerNumber(const std::string §ion, const std::string &key);
static long double extractFloatNumber(const std::string §ion, const std::string &key);
// [section][parameter name][parameter value]
static inline IniStructure values;
static inline std::string iniPath;
};
Ini.cpp
#include "Ini.hpp"
// ------------------------------------------------- Static functions ------------------------------------------------ //
/**
* Parse the INI file located to the given file path and store the values in Ini::values
*
* @param filePath - The path of the INI file to parse
*/
void Ini::parse(const std::string &filePath)
{
std::ifstream fileReader(filePath, std::ifstream::in);
std::string fileContent((std::istreambuf_iterator<char>(fileReader)), std::istreambuf_iterator<char>());
std::regex sections(R"(\[([^\]\r\n]+)]((?:\r?\n(?:[^[\r\n].*)?)*))");
std::regex parameters(R"((\w+) ?= ?(\"([^\"]+)\"|([^\r\n\t\f\v;#]+)))");
std::smatch sectionMatch;
iniPath = filePath;
if (fileReader.fail()) {
throw std::runtime_error("The file " + Ini::iniPath + " could not be opened");
}
while (regex_search(fileContent, sectionMatch, sections)) {
std::unordered_map<std::string, std::string> sectionParameters;
std::string sectionString = sectionMatch[2].str();
for (std::sregex_iterator parameter(sectionString.begin(), sectionString.end(), parameters); parameter != std::sregex_iterator(); ++parameter) {
std::smatch parameterMatch = *parameter;
sectionParameters[parameterMatch[1].str()] = parameterMatch[3].matched ? parameterMatch[3].str() : parameterMatch[4].str();
// parameterMatch[1] is the key, parameterMatch[3] is a trim quoted string (string without double quotes)
// and parameterMatch[4] is a number
}
values[sectionMatch[1].str()] = sectionParameters;
fileContent = sectionMatch.suffix();
}
}
/**
* Tells if the parameter exists in the given section at the given key
*
* @param section - The INI section to get the parameter from
* @param key - The INI parameter key
*
* @return True if the parameter exists in the given section at the given key, false otherwise
*/
bool Ini::keyExists(const std::string §ion, const std::string &key)
{
return values.find(section) != values.end() && values[section].find(key) != values[section].end();
}
/**
* Extract the integer number of the parameter in the given INI section at the given key
*
* @param section - The INI section to get the parameter from
* @param key - The INI parameter key
*
* @return The requested integer number value
*/
long long Ini::extractIntegerNumber(const std::string §ion, const std::string &key)
{
std::smatch intNumberMatch;
std::regex intNumber(R"(^\s*(\-?\d+)\s*$)");
if (!keyExists(section, key)) {
throw std::runtime_error("The key " + key + " does not exist in section " + section + " in " + Ini::iniPath);
}
if (std::regex_match(values[section][key], intNumberMatch, intNumber)) {
return std::strtoll(intNumberMatch[1].str().c_str(), nullptr, 10);
} else {
throw std::runtime_error("The given parameter is not an integer number");
}
}
/**
* Extract the floating point number of the parameter in the given INI section at the given key
*
* @param section - The INI section to get the parameter from
* @param key - The INI parameter key
*
* @return The requested floating point number value
*/
long double Ini::extractFloatNumber(const std::string §ion, const std::string &key)
{
std::smatch floatNumberMatch;
std::regex floatNumber(R"(^\s*(\-?(?:(?:\d+(?:\.\d*)?)|(?:\d+(?:\.\d+)?e(?:\+|\-)\d+))f?)\s*$)");
if (!keyExists(section, key)) {
throw std::runtime_error("The key " + key + " does not exist in section " + section + " in " + Ini::iniPath);
}
if (std::regex_match(values[section][key], floatNumberMatch, floatNumber)) {
return std::strtold(floatNumberMatch[1].str().c_str(), nullptr);
} else {
throw std::runtime_error("The given parameter is not a floating point number");
}
}
// ---------------------------------------------- Template specialisation -------------------------------------------- //
/**
* Throw an exception if the requested type is not defined
*/
template<typename T>
T Ini::get(const std::string §ion, const std::string &key)
{
throw std::runtime_error("The type of the given parameter is not defined");
}
/**
* Get the boolean parameter in the given INI section at the given key
*
* @param section - The INI section to get the parameter from
* @param key - The INI parameter key
*
* @return The requested bool value
*/
template<>
bool Ini::get(const std::string §ion, const std::string &key)
{
if (iniPath.empty()) { parse(); }
return values[section][key] == "true" || values[section][key] == "1";
}
/**
* Get the string parameter in the given INI section at the given key
*
* @param section - The INI section to get the parameter from
* @param key - The INI parameter key
*
* @return The requested string value
*/
template<>
std::string Ini::get(const std::string §ion, const std::string &key)
{
if (iniPath.empty()) { parse(); }
return values[section][key];
}
/**
* Get the short number parameter in the given INI section at the given key
*
* @param section - The INI section to get the parameter from
* @param key - The INI parameter key
*
* @return The requested short number value
*/
template<>
short Ini::get(const std::string §ion, const std::string &key)
{
if (iniPath.empty()) { parse(); }
auto number = extractIntegerNumber(section, key);
if (number >= -SHRT_MAX && number <= SHRT_MAX) {
return static_cast<short>(number);
} else {
throw std::runtime_error("The number is out of range of a short integer");
}
}
/**
* Get the int number parameter in the given INI section at the given key
*
* @param section - The INI section to get the parameter from
* @param key - The INI parameter key
*
* @return The requested int number value
*/
template<>
int Ini::get(const std::string §ion, const std::string &key)
{
if (iniPath.empty()) { parse(); }
auto number = extractIntegerNumber(section, key);
if (number >= -INT_MAX && number <= INT_MAX) {
return static_cast<int>(number);
} else {
throw std::runtime_error("The number is out of range of a an integer");
}
}
/**
* Get the unsigned int number parameter in the given INI section at the given key
*
* @param section - The INI section to get the parameter from
* @param key - The INI parameter key
*
* @return The requested unsigned int number value
*/
template<>
unsigned int Ini::get(const std::string §ion, const std::string &key)
{
if (iniPath.empty()) { parse(); }
auto number = extractIntegerNumber(section, key);
if (number < 0) {
throw std::runtime_error("The number is negative so it cannot be unsigned");
} else if (number <= UINT_MAX) {
return static_cast<unsigned int>(number);
} else {
throw std::runtime_error("The number is out of range of a an unsigned integer");
}
}
/**
* Get the long int number parameter in the given INI section at the given key
*
* @param section - The INI section to get the parameter from
* @param key - The INI parameter key
*
* @return The requested long int number value
*/
template<>
long Ini::get(const std::string §ion, const std::string &key)
{
if (iniPath.empty()) { parse(); }
auto number = extractIntegerNumber(section, key);
if (number >= -LONG_MAX && number <= LONG_MAX) {
return number;
} else {
throw std::runtime_error("The number is out of range of a a long integer");
}
}
/**
* Get the long long int number parameter in the given INI section at the given key
*
* @param section - The INI section to get the parameter from
* @param key - The INI parameter key
*
* @return The requested long long int number value
*/
template<>
long long Ini::get(const std::string §ion, const std::string &key)
{
if (iniPath.empty()) { parse(); }
return extractIntegerNumber(section, key);
}
/**
* Get the float number parameter in the given INI section at the given key
*
* @param section - The INI section to get the parameter from
* @param key - The INI parameter key
*
* @return The requested float number value
*/
template<>
float Ini::get(const std::string §ion, const std::string &key)
{
if (iniPath.empty()) { parse(); }
auto number = extractFloatNumber(section, key);
if (number >= -FLT_MAX && number <= FLT_MAX) {
return static_cast<float>(number);
} else {
throw std::runtime_error("The number is out of range of a float");
}
}
/**
* Get the double number parameter in the given INI section at the given key
*
* @param section - The INI section to get the parameter from
* @param key - The INI parameter key
*
* @return The requested double number value
*/
template<>
double Ini::get(const std::string §ion, const std::string &key)
{
if (iniPath.empty()) { parse(); }
auto number = extractFloatNumber(section, key);
if (number >= -DBL_MAX && number <= DBL_MAX) {
return static_cast<double>(number);
} else {
throw std::runtime_error("The number is out of range of a double");
}
}
/**
* Get the long double number parameter in the given INI section at the given key
*
* @param section - The INI section to get the parameter from
* @param key - The INI parameter key
*
* @return The requested long double number value
*/
template<>
long double Ini::get(const std::string §ion, const std::string &key)
{
if (iniPath.empty()) { parse(); }
return extractFloatNumber(section, key);
}
main.cpp
#include <iostream>
#include "Ini.hpp"
int main(int argc, char **argv)
{
try {
auto myNumber = Ini::get<int>("Section", "parameter1");
auto myString = Ini::get<std::string>("Section", "parameter2");
std::cout << "Get the string " << myString << " and the number " << myNumber << " from the INI config file." << std::endl;
} catch (const std::exception &e) {
std::cerr << e.what() << std::endl;
return 1;
}
return 0;
}
example.ini
[Section]
parameter1 = 10
; comment
parameter2 = My string parameter ; comment
-
1\$\begingroup\$ Yep I did couple of tests in a online regex tester, I'll put some demos. I didn't find a native C++ INI parser in the std like there is in PHP for example. \$\endgroup\$Romain Laneuville– Romain Laneuville2020年11月03日 09:17:16 +00:00Commented Nov 3, 2020 at 9:17
2 Answers 2
General issues to the whole concept:
- There is a reason why
ini
file format is considered deprecated by Microsoft. File formats likejson
andxml
have tree-like structure making them a lot more flexible thatini
file format that has only single-depth sections.
Suppose you want a class to obtain a value from the configuration file. Okay, that's simple just fix a section for the class and let it take value from a given key. But wait, what if you have more than a single instance of the class and you want different configurations for it? Do you want to manually decide the section for each class?
With tree-like configuration file formats you can just go to a subsection and obtain values from there. And everything is organized automatically.
If you still want to operate with INI file format you can just extend it by saying that /
acts as a subsection. So,
[SECTION]
SUBSECTION/KEY = VALUE
is equivalent to
[SECTION/SUBSECTION]
KEY = VALUE
and both resulting in SECTION/SUBSECTION/KEY = VALUE
.
- Making ini parser into a singleton creates a lot of issues.
What do you think will happen if you parse several ini files and some of them share the same sections?
What will happen if in one thread you access data while another thread parses an ini file? You need to make the class thread-safe.
What if you don't want to expose all your ini files to every section in the code? Say you want to instantiate a class from a different version of a specific
ini
file that has same fields but different values as the one you work on currently?
I am a strong believer in Context Pattern - you create a class with some dynamic shared data and forward it to all necessary class instances (only slightly alter handles to it so they access information from the correct places). Singletons are fine for services/functions that require relatively long instantiation. Configuration file is more of a shared data and not a singleton.
- Okay, let's move to various technical issues:
You define template method
get
in .cpp file. That's not gonna work. They must be defined in header else using it for new types causes compilation errors so thethrow
will never be triggered. Honestly, you want a compilation error instead of a runtime error but did you ever read a template error message? They are all incomprehensible.Instead you should write the unspecified
get
in header and put astatic_assert
inside.The interface is very clunky. Frequently, people have default parameters and just want to see if the value was overridden in the configuration file and update it if necessary. Think how easy it would be with this interface. They will have to wrap every single call into try/catch. That's horrible.
Why not simply return a
std::optional<T>
instead of throwing for every little thing? Checkboost::property_tree
how they have it. It is written a lot more sensibly.The methods that just want to get values modify the data, e.g.,
bool Ini::get(const std::string §ion, const std::string &key) { return values[section][key] == "true" || values[section][key] == "1"; }
Here, if
values[section]
doesn't exist it will be created and same forvalues[section][key]
. It is definitely not what you want with "get data" methods. In addition, it will cause data-races and UB issues when you try to get data from the ini class by different threads.Since you want a modern C++ code, why do you use such outdated MACROS? for integer and float limits? There is
std::numeric_limits<T>
for this purpose and you can write a single definition for all integral values instead of specializing it for each type.Accessing keys of
unordered_map<string, unordered_map<string,string>>
is inefficient and inconvenient. To access a single key you have to generate hash twice one for each map. Consider usingunordered_map<string,string>
and store access keys in formatsection/key
instead. This way you'll also be able to forward section + key in a single string instead of two. Furthermore, for input consider usingstring_view
although, I am unsure ifunordered_map<string,string>
can find elements directly fromstring_view
keys.I believe it will be more convenient if the default ini file wasn't a universal constant but instead depend on the name of the executable? no? Like you have two executables
A.exe
andB.exe
thenA.exe
will automatically try to loadA.ini
andB.exe
will automatically try to loadB.ini
. Isn't it better?Ini::parse(const std::string &filePath)
If you already useC++17
then please use dedicated classstd::filesystem::path
instead ofstd::string
. Also the you should utilizestd::move
asvalues[sectionMatch[1].str()] = sectionParameters;
will cause the unordered mapsectionParameters
to be copied which is slow for such a class. Furthermore, it is completely unnecessary as you could've just writtenvalues[sectionMatch[1].str()] = std::move(sectionParameters);
which would steal all its content instead of copying.Honestly, I don't understand the regular expressions and I have some doubts that it will properly parse
ini
files - you should add checks on this part and test it properly. Furthermore, you ought to put out warnings/errors when the file you read doesn't followini
specification and I don't see it done.
-
\$\begingroup\$ Thank you for this long and precise review. INI files intent to be simpler than verbose XML one, I really like JSON format since I was a web developer and used this format quite a lot in JS but JSON format lack of comments in it to describe what's going on. This class is not TS, it should be you're right ! Using a static_assert would be better than error throwing that's a good point. This code came from a C++14 CUDA project and the project can now handle C++17 with NVCC 11 so I will use the std::filesystem. I will get most of your recommendations and post a 2nd version today. \$\endgroup\$Romain Laneuville– Romain Laneuville2020年11月03日 09:13:55 +00:00Commented Nov 3, 2020 at 9:13
-
\$\begingroup\$ I'm curious of one thing, I tried as you suggested to put a static assert in header to have compilation error when a type is not implemented with
std::is_same_v
. It works when I build in debug mode but not in release (-O3). I kept the template specialization in.cpp
file. Must template specialization be located in header file ? To be clear, if I put static_assert in header file in release mode, compilation warns about undefined specialization but fails to get the correct one in function call at execution. The unspecified template is called. \$\endgroup\$Romain Laneuville– Romain Laneuville2020年11月05日 11:15:52 +00:00Commented Nov 5, 2020 at 11:15 -
\$\begingroup\$ @RomainLaneuville you need to either make definition in header or declare via
extern template
that there is a .cpp definition elsewhere. Generally, I thought to inform you later that you shouldn't writeget
like this. Instead, you should makeget
template from classesT
andParser
(defaulted to somedefault_parser<T>
) and let theParser
class convert from the string toT
. It is much easier to specialise classes as opposed to functions. \$\endgroup\$ALX23z– ALX23z2020年11月05日 12:01:48 +00:00Commented Nov 5, 2020 at 12:01
Based on ALX23z recommendations, I did the following changes :
- The class is now thread safe
- There is only one get method that parsing all the types
- The class is now using
std::numeric_limits<T>
instead of old C style constants - The class is now using
std::filesystem::path
instead ofstd::string
for file path parameter
I kept the typedef std::unordered_map<std::string, std::unordered_map<std::string, std::string>> IniStructure
which I think is more readable when accessing parameter as values[section][parameter]
and the parameter access time is not critical as it is not intended to be accessed multiple times like in a loop.
This class does not handle multiple INI files parsing.
I tried to use a static_assert
to warn about undefined type at compilation time but the compilation fails before reaching the assertion.
Here is the new version :
Ini.hpp
#include <filesystem>
#include <fstream>
#include <string>
#include <unordered_map>
#include <regex>
#include <type_traits>
#include <limits>
#include <mutex>
template<typename T> struct unsupportedType : std::false_type { };
struct Ini
{
public:
typedef std::unordered_map<std::string, std::unordered_map<std::string, std::string>> IniStructure;
Ini() = delete;
static void parse(const std::filesystem::path &filePath = PARAMETERS_INI_FILE_PATH);
template<typename T> static T get(const std::string §ion, const std::string &key);
private:
static void checkParameter(const std::string §ion, const std::string &key);
static long long extractIntegerNumber(const std::string §ion, const std::string &key);
static long double extractFloatNumber(const std::string §ion, const std::string &key);
static void checkInit();
// IniStructure[section][parameter name] = parameter value
static inline IniStructure values;
static inline std::filesystem::path iniPath;
static inline std::mutex mutex;
static inline bool initialized = false;
};
Ini.cpp
#include "Ini.hpp"
// ------------------------------------------------- Static functions ------------------------------------------------ //
/**
* Parse the INI file located to the given file path and store the values in Ini::values
*
* @param filePath - The path of the INI file to parse
*/
void Ini::parse(const std::filesystem::path &filePath)
{
std::ifstream fileReader(filePath, std::ifstream::in);
std::string fileContent((std::istreambuf_iterator<char>(fileReader)), std::istreambuf_iterator<char>());
std::regex sections(R"(\[([^\]\r\n]+)]((?:\r?\n(?:[^[\r\n].*)?)*))");
std::regex parameters(R"((\w+) ?= ?(\"([^\"]+)\"|([^\r\n\t\f\v;#]+)))");
std::smatch sectionMatch;
iniPath = filePath;
if (fileReader.fail()) {
throw std::runtime_error("The file " + Ini::iniPath.string() + " could not be opened");
}
while (regex_search(fileContent, sectionMatch, sections)) {
std::unordered_map<std::string, std::string> sectionParameters;
std::string sectionString = sectionMatch[2].str();
for (std::sregex_iterator parameter(sectionString.begin(), sectionString.end(), parameters); parameter != std::sregex_iterator(); ++parameter) {
std::smatch parameterMatch = *parameter;
sectionParameters[parameterMatch[1].str()] = parameterMatch[3].matched ? parameterMatch[3].str() : parameterMatch[4].str();
// parameterMatch[1] is the key, parameterMatch[3] is a trim quoted string (string without double quotes)
// and parameterMatch[4] is a number
}
values[sectionMatch[1].str()] = std::move(sectionParameters);
fileContent = sectionMatch.suffix();
}
initialized = true;
}
/**
* Tells if the parameter exists in the given section at the given key
*
* @param section - The INI section to get the parameter from
* @param key - The INI parameter key
*
* @throw Runtime exception if either the section does not exist or the key does not exists in the given section
*/
void Ini::checkParameter(const std::string §ion, const std::string &key)
{
if (values.find(section) == values.end()) {
throw std::runtime_error("The section " + section + " does not exist in " + Ini::iniPath.string());
}
if (values[section].find(key) == values[section].end()) {
throw std::runtime_error("The key " + key + " does not exist in section " + section + " in " + Ini::iniPath.string());
}
}
/**
* Extract the integer number of the parameter in the given INI section at the given key
*
* @param section - The INI section to get the parameter from
* @param key - The INI parameter key
*
* @return The requested integer number value
*/
long long Ini::extractIntegerNumber(const std::string §ion, const std::string &key)
{
std::smatch intNumberMatch;
std::regex intNumber(R"(^\s*(\-?\d+)\s*$)");
if (std::regex_match(values[section][key], intNumberMatch, intNumber)) {
return std::strtoll(intNumberMatch[1].str().c_str(), nullptr, 10);
} else {
throw std::runtime_error("The given parameter is not an integer number");
}
}
/**
* Extract the floating point number of the parameter in the given INI section at the given key
*
* @param section - The INI section to get the parameter from
* @param key - The INI parameter key
*
* @return The requested floating point number value
*/
long double Ini::extractFloatNumber(const std::string §ion, const std::string &key)
{
std::smatch floatNumberMatch;
std::regex floatNumber(R"(^\s*(\-?(?:(?:\d+(?:\.\d*)?)|(?:\d+(?:\.\d+)?e(?:\+|\-)\d+))f?)\s*$)");
if (std::regex_match(values[section][key], floatNumberMatch, floatNumber)) {
return std::strtold(floatNumberMatch[1].str().c_str(), nullptr);
} else {
throw std::runtime_error("The given parameter is not a floating point number");
}
}
/**
* Check if the INI file is already parsed, if not parse the INI file. This operation is thread safe.
*/
void Ini::checkInit()
{
std::lock_guard<std::mutex> lock(mutex);
if (!initialized) {
parse();
}
}
// ---------------------------------------------- Template specialisation -------------------------------------------- //
/**
* Get the parameter in the given INI section at the given key, extract the value depending on the given type
*
* @param section - The INI section to get the parameter from
* @param key - The INI parameter key
*
* @return The requested parameter value
*/
template<typename T> T Ini::get(const std::string §ion, const std::string &key) {
checkInit();
checkParameter(section, key);
if constexpr (std::is_same_v<T, bool>) {
return values[section][key] == "true" || values[section][key] == "1";
} else if constexpr (std::is_same_v<T, std::string>) {
return values[section][key];
} else if constexpr (std::is_same_v<T, short> || std::is_same_v<T, int> || std::is_same_v<T, unsigned int>
|| std::is_same_v<T, long> || std::is_same_v<T, long long>
) {
auto number = extractIntegerNumber(section, key);
if (std::is_same_v<T, unsigned int> && number < 0) {
throw std::runtime_error("The number is negative so it cannot be unsigned");
} else if (number >= std::numeric_limits<T>::lowest() && number <= std::numeric_limits<T>::max()) {
return static_cast<T>(number);
} else {
throw std::runtime_error("The number is out of range of the given integral type");
}
} else if constexpr (std::is_same_v<T, float> || std::is_same_v<T, double> || std::is_same_v<T, long double>) {
auto number = extractFloatNumber(section, key);
if (number >= std::numeric_limits<T>::lowest() && number <= std::numeric_limits<T>::max()) {
return static_cast<T>(number);
} else {
throw std::runtime_error("The number is out of range of the given floating type");
}
} else {
// @todo This does not warn at compile, compilation fails before reaching this
static_assert(unsupportedType<T>::value, "The INI parser cannot read parameter of the given C++ type");
}
}
// Forward template declarations
template bool Ini::get<bool>(const std::string §ion, const std::string &key);
template std::string Ini::get<std::string>(const std::string §ion, const std::string &key);
template short Ini::get<short>(const std::string §ion, const std::string &key);
template int Ini::get<int>(const std::string §ion, const std::string &key);
template unsigned int Ini::get<unsigned int>(const std::string §ion, const std::string &key);
template long Ini::get<long>(const std::string §ion, const std::string &key);
template long long Ini::get<long long>(const std::string §ion, const std::string &key);
template float Ini::get<float>(const std::string §ion, const std::string &key);
template double Ini::get<double>(const std::string §ion, const std::string &key);
template long double Ini::get<long double>(const std::string §ion, const std::string &key);
-
\$\begingroup\$ Hi, if you want the new code reviewed, you should create a new question on this site. \$\endgroup\$G. Sliepen– G. Sliepen2020年11月05日 19:50:03 +00:00Commented Nov 5, 2020 at 19:50
-
\$\begingroup\$ I just posted the new "solution" after taking some reviews into consideration, i'm not going to post the code x times after each iteration. \$\endgroup\$Romain Laneuville– Romain Laneuville2020年11月05日 21:22:48 +00:00Commented Nov 5, 2020 at 21:22
-
5\$\begingroup\$ Ok, that's fine. However, would you consider marking the answer from @ALX23z as the accepted answer? That is a real review after all, and allowed you to create the revised code you posted here. \$\endgroup\$G. Sliepen– G. Sliepen2020年11月05日 21:42:01 +00:00Commented Nov 5, 2020 at 21:42