One of the methods for working with configuration files in C ++ (Qt)

In almost every project, the task of persistent read / write configuration arises. It is no secret that there are a large number of ready-made libraries for solving this problem. Some of them are simple, some are a little more difficult to use.
If the project is developed using Qt, I think it makes no sense to link an additional library, since Qt has all the means to create a very simple, flexible and cross-platform solution.
Just about this decision I want to tell you in this post.

Introduction


Qt has a very convenient QSettings class . In principle, it is very easy to use:
/*
    main.cpp
*/
int main(int argc, char *argv[]){
    // эти настройки используются (неявно) классом QSettgins для
    // определения имени и местоположения конфига
    QCoreApplication::setOrganizationName("org");
    QCoreApplication::setApplicationName("app");
    ...
    return 0;
}

/*
    some.cpp
*/
void func(){
    QSettings conf;
    ...
    // запись в конфиг
    conf.setValue("section1/key1", someData);   // запись в секцию section1
    conf.setValue("key2", someData2);           // запись в секцию General
    ...
    // чтение из конфига
    QString strData = conf.value("section1/key1").toString();
}

From the above example, the usual use of QSettings , you can immediately see extensibility and code support issues:
  1. If the key names are explicitly written in the code, then in the future we may encounter a situation where it will be difficult to delete / add new configuration keys. Those. with this approach, the problem here is that it is impossible to catch invalid keys at the compilation stage.
  2. To avoid problem # 1, we could write all the keys into a separate header file, and access them through string constants. To improve the modularity of the code and clean up the global scope, it would also be worth putting all the keys in a separate namespace.
    namespace Settings{
        const char * const key1 = "key1";
        const char * const section1_key1 = "section1/key1";
        const char * const section1_key2 = "section1/key2";
    }
    

    But here we have another, not very pleasant detail:
    * firstly too verbose, i.e. information is duplicated (key1 -> "key1", etc.). In principle, this is not surprising, since we somehow have to describe the serialization of key names. Yes, we could write a macro, but for well-known reasons, macros should be avoided, especially if there are alternative options.
    * secondly, with a sufficient number of keys and sections, it is likely that you will have to register constants for all combinations, which is not very convenient. Of course, we can get the constants for the keys and for the sections separately, but then, with each call to QSettings , we will have to combine the strings.

If you carefully review all the above problems again, you can conclude: the key is represented by a string - this is the main problem. Indeed, if we use enums ( enum s) as the key , then all of the above disappears at once.

Enumerations are certainly convenient, but QSettings requires, as a parameter of the key, a string. Those. we need some mechanism that would enable us to translate the values ​​of the enumerations into strings (extract the string values ​​of the elements of the enumerations). For example, from the following enumeration:
enum Key{
    One,
    Two,
    Three
};

you need to somehow extract 3 lines: "One", "Two", "Three".
Unfortunately, using standard C ++ tools, this is not possible. But what to do?
Here Qt comes to the rescue with its meta-object model, or rather QMetaEnum . I will not write , since this is a separate topic. I can only give links: one , two .

Implementation


With QMetaEnum in service , we can now implement the Settings class , devoid of all the above disadvantages, as well as providing the ability to specify default settings. The Settings class is a singleton Meyers, this gives us the ease of configuration and its use:

settings.h (Expand Spoiler)
/*
    settings.h
*/

#ifndef SETTINGS_H
#define SETTINGS_H

#include <QVariant>
#include <QSettings>
#include <QMetaEnum>

/**
  @brief Синглтон для доступа к конфигурации

  Usage:
  @code
    ...
    ...
    //пердварительная настройка (должен быть где-нибуль в main)
    QApplication::setOrganizationName("Organization name");
    QApplication::setApplicationName("App name");
    ...
    ...
    //установка значений по умолчанию (строка может быть многострочной)
    Settings::setDefaults("SomeKey: value1; SomeSection/SomeKey: value2");

    //или так
    QFile f(":/defaults/config");
    f.open(QIODevice::ReadOnly);
    Settings::setDefaults(f.readAll());
    ...
    ...
    void fun(){
        ...
        QVariant val1 = Settings::get(Settings::SomeKey);
        Settings::set(Settings::SomeKey) = "new val1";
        ...
        QVariant val2 = Settings::get(Settings::SomeKey, Settings::SomeSection);
        Settings::set(Settings::SomeKey, Settings::SomeSection) = "new val2";
        ...
    }
  @endcode
*/
class Settings{
    Q_GADGET
    Q_ENUMS(Section)
    Q_ENUMS(Key)
public:
    enum Section{
        General,
        Network,
        Proxy
    };

    enum Key{
        URI,
        Port,
        User,
        Password
    };

    class ValueRef{
    public:
        ValueRef(Settings &st, const QString &kp) :
            parent(st), keyPath(kp){}
        ValueRef & operator = (const QVariant &d);
    private:
        Settings &parent
        const QString keyPath;
    };

    static void setDefaults(const QString &str);
    static QVariant get(Key, Section /*s*/ = General);
    static ValueRef set(Key, Section /*s*/ = General);

private:
    QString keyPath(Section, Key);

    static Settings & instance();
    QMetaEnum keys;
    QMetaEnum sections;
    QMap<QString, QVariant> defaults;
    QSettings conf;

    Settings();
    Settings(const Settings &);
    Settings & operator = (const Settings &);
};

#endif // SETTINGS_H

settings.cpp (Expand spoiler)
/*
    settings.cpp
*/

#include "settings.h"
#include <QSettings>
#include <QMetaEnum>
#include <QRegExp>
#include <QStringList>

Settings::Settings(){
    const QMetaObject &mo = staticMetaObject;
    int idx = mo.indexOfEnumerator("Key");
    keys = mo.enumerator(idx);

    idx = mo.indexOfEnumerator("Section");
    sections = mo.enumerator(idx);
}

QVariant Settings::get(Key k, Section s){
    Settings &self = instance();
    QString key = self.keyPath(s, k);
    return self.conf.value(key, self.defaults[key]);
}

Settings::ValueRef Settings::set(Key k, Section s){
    Settings &self = instance();
    return ValueRef(self, self.keyPath(s, k));
}

void Settings::setDefaults(const QString &str){
    Settings &self = instance();

    //section/key : value
    //section - optional
    QRegExp rxRecord("^\\s*(((\\w+)/)?(\\w+))\\s*:\\s*([^\\s].{0,})\\b\\s*$");

    auto kvs = str.split(QRegExp(";\\W*"), QString::SkipEmptyParts); //key-values
    for(auto kv : kvs){
        if(rxRecord.indexIn(kv) != -1){
            QString section = rxRecord.cap(3);
            QString key = rxRecord.cap(4);
            QString value = rxRecord.cap(5);

            int iKey = self.keys.keyToValue(key.toLocal8Bit().data());
            if(iKey != -1){
                int iSection = self.sections.keyToValue(section.toLocal8Bit().data());
                if(section.isEmpty() || iSection != -1){
                    self.defaults[rxRecord.cap(1)] = value;
                }
            }
        }
    }
}

//Settings::ValueRef-----------------------------------------------------------
Settings::ValueRef & Settings::ValueRef::operator = (const QVariant &data){
    parent.conf.setValue(keyPath, data);
    return *this;
}


//PRIVATE METHODS--------------------------------------------------------------
QString Settings::keyPath(Section s, Key k){
    auto szSection = sections.valueToKey(s);
    auto szKey = keys.valueToKey(k);
    return QString(s == General ? "%1" : "%2/%1").arg(szKey).arg(szSection);
}

Settings & Settings::instance(){
    static Settings singleton;
    return singleton;
}

In this implementation, the QSettings class is used exclusively for cross-platform access to settings. Of course, if desired, QSettgins can be replaced by any other mechanism, such as SQLite .

Usage example


The Settings class provides a very simple and convenient interface consisting of only three static methods:
void setDefaults(const QString &str); - setting default parameters
QVariant get(Key, Section); - reading the value (section can be omitted)
ValueRef set(Key, Section); - writing the value (section can be omitted)

/*
    main.cpp
*/
#include <QtCore/QCoreApplication>
#include <QUrl>
#include <QFile>
#include "settings.h"

void doSome(){
    //чтение из секции General
    QString login = Settings::get(Settings::User).toString();    // login == "unixod"
    
    QUrl proxyUrl = Settings::get(Settings::URI, Settings::Proxy).toUrl();    // http://proxy_uri

    QString generalUrl = Settings::get(Settings::URI).toString();    // пусто
    if(generalUrl.isEmpty())
        Settings::set(Settings::URI) = "http://some_uri";
}

int main(int argc, char *argv[]){
    //данные параметры используются QSettings для определения куда сохранять конфигурацию
    QCoreApplication::setOrganizationName("unixod");
    QCoreApplication::setApplicationName("app");

    //по желанию можем установить дефолтную конфигурацию:
    QFile cfgDefaults(":/config/default.cfg");  // я обычно дефолтовые настройки помещаю в ресурсы
    cfgDefaults.open(QIODevice::ReadOnly);
    Settings::setDefaults(cfgDefaults.readAll());
    //...
    doSome();
    //...
    return 0;
}

Here is an example of the default settings description syntax:

default.cfg (Expand Spoiler)
Proxy/URI: http://proxy_uri;
User: unixod;

as you can see the format is simple:
[section name]/key : value;

Conclusion


It is worth noting that this Settings class is easily expandable. Those. if desired, add / remove / rename any keys or sections, just change the corresponding enum !

The reader may ask if it is possible to somehow put out the general logic “out of brackets”.
Answer: it is possible but not better. Since the Qt meta-object model does not work with templates, you will have to use macros, which in turn entails known problems:
  • Debugging complexity
  • Difficulty code analysis for the IDE
  • Difficulty reading, code
  • etc.

When building, do not forget to enable C ++ 11 support:
  • GCC:
    -std = c ++ 0x
  • Qt project file:
    QMAKE_CXXFLAGS + = -std = c ++ 0x

Thanks for your attention. )