Advice on Best Approach, Singleton Class for User Settings?

Hello,

I am wondering if I can get advice on the best approach on working with user settings.

My current idea of how to work with user settings goes like this:

1. Create a class to hold all of the user settings.
2. Use that class to load/save/hold settings in memory.
3. Create an instance of that class once in the entry point of the program (int Main or whatever).
4. Pass, by reference this same class instance around to all of the other classes that need the user settings.
5. Once all other objects deleted, save and then delete the User Settings class.

I created a psuedo-code example below of this.

My question is if this is the best way or should I be doing something else.

In particular, I am wondering if somehow I can avoid passing the settings class by reference all of the time.

Would this be a good case scenario for a "Singleton" type class?

Thank you for your time.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94

#include <string>

class UserSettings {

private:
	std::string SettingOne;
	int SettingTwo;
	bool SettingThree:
	
	std::string UserSettingsPath;
	
public:
	UserSettings(); //Set up defaults here.
	~UserSettings();
	
	bool LoadUserSettings(std::string PathToUserSettings); // Loads User Settings From File.
	
	bool SaveUserSettings(std::string PathToSaveUserSettings); //Saves settings to specified location.
	
	bool SaveUserSettings(); //Saves settings to location specified when loading settings.
	
	//Functions to Get and Set Settings.
	
	std::string GetSettingOne();
	void SetSettingOne(std::string ValueToSet);
	
	int GetSettingTwo();
	void SetSettingTwo(int ValueToSet);
	
	bool GetSettingThree();
	void SetSettingThree(bool ValueToSet);

};

class FirstWorkerClass {

public:

	FirstWorkerClass(UserSettings *CurrentSettings);
	~FirstWorkerClass();
	
	DoSomeWork();

};

class SecondWorkerClass {

public:

	SecondWorkerClass(UserSettings *CurrentSettings);
	~SecondWorkerClass();
	
	DoSomeWork();

};

UserSettings *MainSettings;
FirstWorkerClass *FirstWorker;
SecondWorkerClass *SecondWorker;

int main(int argc, char** argv) {

	MainSettings = new UserSettings();
	
	if (!MainSettings->LoadUserSettings("PathToMySettings.ini")) {
	
		return EXIT_FAILURE;
		
	}
	
	FirstWorker = new FirstWorkerClass(MainSettings);
	
	FirstWorker->DoSomeWork();
	
	SecondWorker = new SecondWorkerClass(MainSettings);
	
	SecondWorker->DoSomeWork();

	delete FirstWorker;
	
	delete SecondWorker;
	
	if (!MainSettings->SaveUserSettings()) {
	
		return EXIT_FAILURE;
	
	}
	
	delete MainSettings;
	
	return EXIT_SUCCESS;
	
}
closed account (3hM2Nwbp)
A non-singleton approach is certainly more modular and robust. Personally, I avoid singletons whenever possible in order to keep APIs as flexible as possible for future growth. Who's to say that you are never going to want to keep dual configurations in memory at the same time?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class config 
{
  config();
  config(const string& path);

};

//operators for loading / saving config
ostream& operator<<(ostream& s, const config& conf);
istream& operator>>(istream& s, config& conf);
class worker
{
  worker(config conf = /*reasonable default*/);
};

PS. I would also advise against multipart initialization and provide constructors to accomplish the goal in one step.
Last edited on
That's a good possibility, but then that means I have to pass by reference my settings class instance wherever it is needed?

Just seems like a lot of additional code for me to adjust/write just to pass the data around as I show above...
closed account (3hM2Nwbp)
You can fill in a reasonable default that is transparent to your users. This could even be a global default object.

Pardon my brevity, I'm using a tiny mobile phone to peck out messages on.
Last edited on
No problem! Glad you are taking the time to help me out :)

I appreciate it.

Other opinions welcome as well...
user_settings.h
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#ifndef USER_SETINGS_H_WAS_ALREADY_INCLUDED
#define USER_SETINGS_H_WAS_ALREADY_INCLUDED

#include <string>

namespace user_settings {

    const char* const default_path = "default_path.rc" ;

    bool load( std::string path = default_path ) ;
    bool save( std::string path = default_path ) ;

    struct config {

        std::string SettingOne;
        int SettingTwo;
        bool SettingThree;
        std::string UserSettingsPath;
    };

    const char* const default_user = "default_user" ;

    config configuration( std::string user = default_user ) ;
    bool configuration( config values, std::string user = default_user ) ;
}

#endif // USER_SETINGS_H_WAS_ALREADY_INCLUDED 


user_settings.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
#include "user_setings.h"
#include <map>
#include <fstream>

namespace user_settings {

    namespace {

         std::map< std::string, config > settings // map user name => configuration
         { { default_user, { "one", 0, true, "/usr/default_user/user_settings" } } }; // defaults
    }

    config configuration( std::string user ) {

        const auto iter = settings.find(user) ;
        return iter != settings.end() ? iter->second : settings[default_user] ;
    }

    bool configuration( config values, std::string user )
    {
        // validate, if invalid return false
        settings[user] = values ;
        return true ;
    }

    bool load( std::string path ) {

           std::ifstream file(path) ;
           if( !file.is_open() ) return false ;

           // load from file, insert into map

           return true ;
    }

    bool save( std::string path )  {

           std::ofstream file(path) ;
           if( !file.is_open() ) return false ;

           // write values into file

           return true ;
    }

}


And then:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct first_worker_class
{
    first_worker_class( std::string user =  user_settings::default_user ) : user(user) {}
    
    // ....
    
    void do_some_work()
    {
         const auto configuration = user_settings::configuration(user) ;

         // use configuration
    }

    const std::string user ;
};

etc.
Thanks @JLBorges, this seems like a good implementation.

One question though, why use const in this case (in your last code block)?

If I wish to use a member function to write back configuration I can't do that, right? Or am I incorrect?
> why use const in this case

The snippet was written under the assumption was that this function would not need to modify the configuration for the user. Otherwise, it need not be a const.


> If I wish to use a member function to write back configuration I can't do that, right?

Well, you can create another configuration object. For instance:
user_settings::configuration( { "modified SettingOne", 234, false, configuration.UserSettingsPath }, user ) ;
Thanks, I like your second example with another configuration object, that would be the beginning of asynchronous read/write of configuration data, correct?

However, I would need to be constantly accessing the configuration data on disk for that.

Otherwise perhaps I shouldn't use const so that I can keep things in memory and just do I/O on loading and saving of configuration data at critical moments, like when Opening/Closing the application and when people hit up the Options/Preferences dialog of my application.
Well, in the snippet I posted the configuration is held in an in-memory data structure.
std::map< std::string, config > settings // map user name => configuration

user_settings::configuration() just updates the configuration in memory.

It is written out / read from persistent storage only when save() / load() is called.

Since value semantics is used, what a component receives is a snapshot of the configuration at a particular point in time. It can modify its local copy of the configuration without affecting the rest of the program. A configuration change is percolated to the rest of the program only when user_settings::configuration() is called with a modified configuration. Configuration changes made by other components remain invisible till a fresh copy of the configuration is retrieved with a call to user_settings::configuration()

Reference semantics would lead to a quite different scenario.


> just do I/O on loading and saving of configuration data at critical moments, like when Opening/Closing the application and when people hit up the Options/Preferences dialog of my application.

Yes.
Topic archived. No new replies allowed.