Retrieving class types from file efficiently

Suppose your program has many concrete subtypes of Person, and each person will have their own file saved, with their type stored in that file. When reading the files to create the people again (of their proper types), what is the best method to maximize performance (and elegance by shortening the code)? Here is what I have so far. First I used if statements, which is terrible, and now I've improved the performance logarithmically using std::map. I still suspect there is a better way, especially if there are going to be hundreds of different classes. If you want to test it, you can change the PATH constant to whatever path you want, or just leave it as an empty string, and the files will be created in the same directory as your cpp file. The part I'm trying to improve is pointed out in the comments.
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
struct Person {
	std::string name;
	Person (const std::string& newName) : name (newName) {}
	virtual ~Person() = default;
};

struct Guy : Person {using Person::Person;};
struct Girl : Person {using Person::Person;};
struct Boxer : Guy {using Guy::Guy;};
struct Cheerleader : Girl {using Girl::Girl;};

class System {
	private:
		static const std::string PATH, DATABASE, SUFFIX;
		static const std::map<std::string, void(*)(const std::string&)> classNamesMap;
		static std::list<Person*> personnel;
	public:
		static System& GetInstance() {static System* instance = new System;  return *instance;} 
		static inline void initialize();
		static inline void loadDatabase();
		static inline void saveDatabase();
	private:
		System() {};  System(const System&);  const System& operator = (const System&);  ~System() {};
		static void insertGuy (const std::string& name) {Guy* x = new Guy(name);  personnel.emplace_back(x);}
		static void insertGirl (const std::string& name) {Girl* x = new Girl(name);  personnel.emplace_back(x);}
		static void insertBoxer (const std::string& name) {Boxer* x = new Boxer(name);  personnel.emplace_back(x);}
		static void insertCheerleader (const std::string& name) {Cheerleader* x = new Cheerleader(name);  personnel.emplace_back(x);}
};
std::list<Person*> System::personnel;
const std::string System::PATH = "", System::DATABASE = "Database", System::SUFFIX = ".txt";  // put whatever PATH you want
const std::map<std::string, void(*)(const std::string&)> System::classNamesMap = { 
	{typeid(Guy).name(), &System::insertGuy}, {typeid(Girl).name(), &System::insertGirl},
	{typeid(Boxer).name(), &System::insertBoxer}, {typeid(Cheerleader).name(), &System::insertCheerleader}
};

inline void System::initialize() {
	char yesNo;
	std::cout << "New database? (y/n) ";  std::cin >> yesNo;
	if (yesNo == 'y')
	{
		std::ofstream file (PATH + DATABASE + SUFFIX);
		personnel = {new Guy ("John Doe"), new Girl ("Madame Yes"), new Boxer ("Rocky Balboa"), new Cheerleader ("Mary May")};
	}
	else
		loadDatabase();
}

inline void System::saveDatabase() {
	std::ofstream outfile;
	outfile.open ((PATH + DATABASE + SUFFIX).c_str());
	for (Person* x : personnel)
	{
		outfile << x->name << std::endl;
		std::ofstream profile (x->name + ".txt");
		profile << x->name << std::endl;
		profile << typeid(*x).name();
	}
}

inline void System::loadDatabase() {
	std::ifstream infile (PATH + DATABASE + SUFFIX);
	if (infile.fail()) {std::cout << "No such file exists." << std::endl;  exit(0);}
	std::string name, className;
	std::list<std::string> listOfNames;
	while (!infile.eof()) {
		std::getline (infile, name);
		listOfNames.emplace_back(name);
	}
	listOfNames.remove("");
	for (const std::string& x : listOfNames) {
		std::ifstream profile (PATH + x + SUFFIX);
		if (profile.fail()) {std::cout << PATH + x + SUFFIX << " does not exist." << std::endl;  exit(0);}
		std::getline (profile, name);
		std::getline (profile, className);
		auto it = classNamesMap.find(className);  // This is the part I'm trying to improve
			// Is looking up a map the best way to do it?
		if (it == classNamesMap.end()) {std::cout << "Error! Programmer forgot about " << className << std::endl;}
		(*it->second)(name);
	}
	std::cout << "Personnel retrieved:" << std::endl;
	for (Person* x : personnel)
		std::cout << x->name << ", " << typeid(*x).name() << std::endl;
	// Now do whatever with the personnel members, since they have been created of the correct type.
}
		
int main() {
	System::initialize();
	System::saveDatabase();
	std::cin.get(); std::cin.get();
}
Last edited on
Are there going to be hundreds of different classes?

I suppose the fastest way would be to generate a perfect hash table at initialization. This only really makes sense if there's going to be an enormous number of classes and an even larger number of objects.
I'll look into what a perfect hash table consists of. Meanwhile, I just realized that the above simplified version of my program assumes that all the Person concrete classes have the same constructor parameters (in the above case, std::string). Hence the idea
std::map<std::string, void(*)(const std::string&)> classNamesMap;
will not work if all the different Person subtypes have different constructor parameters, so my idea of using std::map won't even work.

Let's assume the classes are now:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
struct Person {
	std::string name;
	Person (const std::string& newName) : name(newName) {}
	virtual ~Person() = default;
};

struct Guy : Person {
	int coolness;
	Guy (const std::string& name, int a) : Person(name), coolness(a) {} 
};

struct Girl : Person {
	int beauty, cupsize;
	Girl (const std::string& name, int a, int b) : Person(name), beauty(a), cupsize(b) {}
};

//struct Boxer : Guy  { Boxer (const std::string& name, int a, int, int) : Guy (name, a) {} };
//struct Cheerleader : Girl { Cheerleader (const std::string& name, int a, int b, int, int) : Girl (name, a, b) {} }; 


The acyclic visitor pattern that JLBorges taught me comes to mind again. I'll work on that now... But I think the accept and visit functions need a second parameter for the std::ofstream object, e.g.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
virtual bool accept (Person::Visitor& visitor, std::ofstream& outfile) override {
	Guy::Visitor* v = dynamic_cast<Guy::Visitor*>(&visitor);
	return v ? v->visit (this, outfile) : false;
}
// ....
struct SaveVisitor : Person::Visitor, Guy::Visitor, Girl::Visitor, Boxer::Visitor, Cheerleader::Visitor {
	virtual bool visit (Guy* guy, std::ofstream& outfile) override {
		outfile << guy->coolness << std::endl;
		return true;
	}
	virtual bool visit (Girl* girl, std::ofstream& outfile) override {
		outfile << girl->beauty << std::endl;
		outfile << girl->cupsize << std::endl;
		return true;
	}
    // etc...
}; 
// and similarly for LoadVisitor : Person::Visitor, Guy::Visitor, Girl::Visitor, Boxer::Visitor, Cheerleader::Visitor
// using std::ifstream& as second parameter for visit. 

At this point, I'm wondering how to treat the above repetitions in data members due to inheritance (e.g. Boxer is repeating Guy's data members, and Cheerleader is repeating Girl's data members). But one step at a time... I'll work on that refinement later.
Last edited on
Done! A slightly modified acyclic visitor pattern.

Now I have to figure out how to remove the repeated lines in the visit overrides caused by the inheritance of Boxer from Guy and Cheerleader from Girl (there will be many such repeated lines with my hundreds of polymorphic classes). And adding a new data member to a base class will create the problem of needing to add the new lines in all the visit overrides for the derived classes. Perhaps macros is the only way.
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
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
#include <iostream>
#include <fstream>
#include <string>
#include <list>
#include <map>
#include <algorithm>
#include <typeinfo>

struct Person {
	std::string name;
	struct Visitor {virtual ~Visitor() = default;};

	Person() = default;
	Person (const std::string& newName) : name(newName) {}
	virtual ~Person() = default;
	virtual bool accept (Visitor&, std::ofstream&) = 0;
	virtual bool accept (Visitor&, std::ifstream&) = 0;
};

#define VISITOR_AND_ACCEPT(ClassName) \
	struct Visitor { \
		virtual ~Visitor() = default; \
		virtual bool visit (ClassName*, std::ofstream&) {return false;}  /* cannot be pure anymore */ \
		virtual bool visit (ClassName*, std::ifstream&) {return false;}  /* cannot be pure anymore */ \
	}; \
	virtual bool accept (Person::Visitor& visitor, std::ofstream& outfile) override { \
		ClassName::Visitor* v = dynamic_cast<ClassName::Visitor*>(&visitor); \
		return v ? v->visit (this, outfile) : false; \
	} \
	virtual bool accept (Person::Visitor& visitor, std::ifstream& infile) override { \
		ClassName::Visitor* v = dynamic_cast<ClassName::Visitor*>(&visitor); \
		return v ? v->visit (this, infile) : false; \
	}

struct Guy : Person {
	VISITOR_AND_ACCEPT(Guy)
	int coolness;
	Guy() = default;
	Guy (const std::string& name, int a) : Person(name), coolness(a) {}
};

struct Girl : Person {
	VISITOR_AND_ACCEPT(Girl)
	int beauty, cupsize;
	Girl() = default;
	Girl (const std::string& name, int a, int b) : Person(name), beauty(a), cupsize(b) {}
};

struct Boxer : Guy  {
	VISITOR_AND_ACCEPT(Boxer)
	int punchingStrength, numKnockouts;
	Boxer() = default;
	Boxer (const std::string& name, int a, int b, int c) : Guy(name, a), punchingStrength(b), numKnockouts(c) {} 
};

struct Cheerleader : Girl {
	VISITOR_AND_ACCEPT(Cheerleader)
	int cheeringLoudness, batonSkill;
	Cheerleader() = default;
	Cheerleader (const std::string& name, int a, int b, int c, int d) : Girl(name, a, b),
		cheeringLoudness(c), batonSkill(d) {}
};

class System {
	public:
		struct SaveVisitor;
		struct LoadVisitor;
	private:
		static const std::string PATH, DATABASE, SUFFIX;
		static const std::map<std::string, Person*> classMap;
		static std::list<Person*> personnel;
	public:
		static System& GetInstance() {static System* instance = new System;  return *instance;} 
		static inline void initialize();
		static inline void loadDatabase();
		static inline void saveDatabase();
	private:
		System() {};  System(const System&);  const System& operator = (const System&);  ~System() {};
};
std::list<Person*> System::personnel;
const std::string System::PATH = "", System::DATABASE = "Database", System::SUFFIX = ".txt";  // put whatever PATH you want
const std::map<std::string, Person*> System::classMap = {
	{typeid(Guy).name(), new Guy}, {typeid(Girl).name(), new Girl}, {typeid(Boxer).name(), new Boxer}, {typeid(Cheerleader).name(), new Cheerleader}	
};

struct System::SaveVisitor : Person::Visitor, Guy::Visitor, Girl::Visitor, Boxer::Visitor, Cheerleader::Visitor {
	virtual bool visit (Guy* guy, std::ofstream& outfile) override {
		outfile << guy->coolness << std::endl;
		return true;
	}
	virtual bool visit (Girl* girl, std::ofstream& outfile) override {
		outfile << girl->beauty << std::endl;
		outfile << girl->cupsize << std::endl;
		return true;
	}
	virtual bool visit (Boxer* boxer, std::ofstream& outfile) override {
		outfile << boxer->coolness << std::endl;
		outfile << boxer->punchingStrength << std::endl;
		outfile << boxer->numKnockouts << std::endl;
		return true;
	}
	virtual bool visit (Cheerleader* cheerleader, std::ofstream& outfile) override {
		outfile << cheerleader->beauty << std::endl;
		outfile << cheerleader->cupsize << std::endl;
		outfile << cheerleader->cheeringLoudness << std::endl;
		outfile << cheerleader->batonSkill << std::endl;
		return true;
	}
};

struct System::LoadVisitor : Person::Visitor, Guy::Visitor, Girl::Visitor, Boxer::Visitor, Cheerleader::Visitor {
	virtual bool visit (Guy* guy, std::ifstream& infile) override {
		std::string name;
		int coolness;
		std::getline (infile, name);
		infile >> coolness;
		*guy = Guy (name, coolness);
		personnel.emplace_back (guy);
		return true;
	}
	virtual bool visit (Girl* girl, std::ifstream& infile) override {
		std::string name;
		int beauty, cupsize;
		std::getline (infile, name);
		infile >> beauty;
		infile >> cupsize;
		*girl = Girl (name, beauty, cupsize);
		personnel.emplace_back (girl);
		return true;
	}
	virtual bool visit (Boxer* boxer, std::ifstream& infile) override {
		std::string name;
		int coolness, punchingStrength, numKnockouts;
		std::getline (infile, name);
		infile >> coolness;
		infile >> punchingStrength;
		infile >> numKnockouts;
		*boxer = Boxer (name, coolness, punchingStrength, numKnockouts);
		personnel.emplace_back (boxer);
		return true;
	}
	virtual bool visit (Cheerleader* cheerleader, std::ifstream& infile) override {
		std::string name;
		int beauty, cupsize, cheeringLoudness, batonSkill;
		std::getline (infile, name);
		infile >> beauty;  // some repeats here, but whatever
		infile >> cupsize;
		infile >> cheeringLoudness;
		infile >> batonSkill;
		*cheerleader = Cheerleader (name, beauty, cupsize, cheeringLoudness, batonSkill);
		personnel.emplace_back (cheerleader);
		return true;
	}		
};

inline void System::initialize() {
	char yesNo;
	std::cout << "New database? (y/n) ";  std::cin >> yesNo;
	if (yesNo == 'y')
	{
		std::ofstream file (PATH + DATABASE + SUFFIX);
		personnel = {
			new Guy ("John Doe", 8), new Girl ("Madame Yes", 9, 3),
			new Boxer ("Rocky Balboa", 6, 9, 20), new Cheerleader ("Mary May", 10, 5, 8, 9)
		};
	}
	else
		loadDatabase();
}

inline void System::saveDatabase() {
	std::ofstream outfile;
	outfile.open ((PATH + DATABASE + SUFFIX).c_str());
	SaveVisitor saveVisitor;
	for (Person* x : personnel)
	{
		outfile << x->name << std::endl;
		std::ofstream profile (x->name + ".txt");
		profile << typeid(*x).name() << std::endl;
		profile << x->name << std::endl;
		x->accept (saveVisitor, profile);  // The magical line!
	}
}

inline void System::loadDatabase() {
	std::ifstream infile (PATH + DATABASE + SUFFIX);
	if (infile.fail()) {std::cout << "No such file exists." << std::endl;  exit(0);}
	std::string name, className;
	std::list<std::string> listOfNames;
	while (!infile.eof()) {
		std::getline (infile, name);
		listOfNames.emplace_back(name);
	}
	listOfNames.remove("");
	LoadVisitor loadVisitor;
	for (const std::string& x : listOfNames) {
		std::ifstream profile (PATH + x + SUFFIX);
		if (profile.fail()) {std::cout << PATH + x + SUFFIX << " does not exist." << std::endl;  exit(0);}
		std::getline (profile, className);
		classMap.find(className)->second->accept (loadVisitor, profile);  // The magical line!
	}
	std::cout << "Personnel retrieved:" << std::endl;
	for (Person* x : personnel)
		std::cout << x->name << ", " << typeid(*x).name() << std::endl;
	// Now do whatever with the personnel members, since they have been created of the correct type.
}
		
int main() {
	System::initialize();
	System::saveDatabase();
	std::cin.get(); std::cin.get();
}
Last edited on
> remove the repeated lines in the visit overrides ...
> adding a new data member to a base class will create the problem ...

Write load/save functions in each class to handle ints own member variables.
And then, in the derived class load/save, first call the base class load/save.

The exemplar idiom (prototype design pattern) is typically implemented this way:
Chapter 8. Programming with Exemplars in C++, 'Advanced C++ Programming Styles and Idioms' by James Coplien

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
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
#include <iostream>
#include <fstream>
#include <string>
#include <memory>
#include <list>

struct Person {
    std::string name;
	Person (const std::string& newName) : name(newName) {}
	virtual ~Person() = default;

    virtual bool is( std::string tag ) const { return tag == class_tag() ;  }
	virtual void load( std::istream& stm ) { std::getline( stm, name ) ; }
	virtual void save( std::ostream& stm ) const { stm << class_tag() << '\n' ; do_save_(stm) ; }

	static std::unique_ptr<Person> create( std::istream& stm ) ;

	protected:
        virtual void do_save_( std::ostream& stm ) const { stm << name << '\n' ; }
	    struct exemplar {} ; Person( exemplar ) ; // only for creating prototypical objects
	    Person() = default ;

	private:
	    static Person prototype ;
        virtual std::string class_tag() const { return "Person" ; }
        virtual Person* do_create( std::istream& stm ) {
            auto p = new Person ;
            p->load(stm) ;
            return p ;
        }
};

Person Person::prototype{ exemplar{} } ;

namespace {
    constexpr std::size_t MAX_CLASSES = 2048 ;
    std::size_t n_classes = 0 ;
    Person* exemplars[MAX_CLASSES] {} ; // forms a chain of responsibility
}

Person::Person( Person::exemplar ) : name( "protype object" ) { exemplars[n_classes++] = this ; }

std::unique_ptr<Person> Person::create( std::istream& stm ) {
     std::string tag ;
     while( std::getline( stm, tag ) && tag.empty() ) ;
     for( std::size_t i = 0 ; i < n_classes ; ++i ) {
         if( exemplars[i]->is(tag) ) return std::unique_ptr<Person>( exemplars[i]->do_create(stm) ) ;
     }

     return nullptr ;
}

struct Guy : Person {
    int coolness;
    Guy (const std::string& name, int a) : Person(name), coolness(a) {}

    virtual void load( std::istream& stm ) override {
        Person::load(stm) ;
        stm >> coolness >> std::skipws ;
    }
    virtual void save( std::ostream& stm ) const { stm << class_tag() << '\n' ; do_save_(stm) ; }

    protected:
        virtual void do_save_( std::ostream& stm ) const override {
            Person::do_save_(stm) ;
            stm << coolness << '\n' ;
        }

        Guy( exemplar e ) : Person(e) {};
	    Guy() = default ;

    private:
        static Guy prototype ;
        virtual std::string class_tag() const override { return "Guy" ; }
        virtual Person* do_create( std::istream& stm ) override {
            auto p = new Guy ;
            p->load(stm) ;
            return p ;
        }
};

Guy Guy::prototype{ exemplar{} } ;

struct Girl : Person {
    int beauty, cupsize;
    Girl (const std::string& name, int a, int b) : Person(name), beauty(a), cupsize(b) {}

    virtual void load( std::istream& stm ) override {
        Person::load(stm) ;
        stm >> beauty >> cupsize >> std::skipws ;
    }
    virtual void save( std::ostream& stm ) const { stm << class_tag() << '\n' ; do_save_(stm) ;}

    protected:
        virtual void do_save_( std::ostream& stm ) const override {
            Person::do_save_(stm) ;
            stm << beauty << ' ' << cupsize << '\n' ;
        }

        Girl( exemplar e ) : Person(e) {};
	    Girl() = default ;

    private:
        static Girl prototype ;
        virtual std::string class_tag() const override { return "Girl" ; }
        virtual Person* do_create( std::istream& stm ) override {
            auto p = new Girl ;
            p->load(stm) ;
            return p ;
        }
};

Girl Girl::prototype{ exemplar{} } ;

struct Boxer : Guy  {
    int punchingStrength, numKnockouts;
    Boxer (const std::string& name, int a, int b, int c)
        : Guy(name, a), punchingStrength(b), numKnockouts(c) {}

    virtual void load( std::istream& stm ) override {
        Guy::load(stm) ;
        stm >> punchingStrength >> numKnockouts >> std::skipws ;
    }
    virtual void save( std::ostream& stm ) const { stm << class_tag() << '\n' ; do_save_(stm) ;}

    protected:
        virtual void do_save_( std::ostream& stm ) const override {
            Guy::do_save_(stm) ;
            stm << punchingStrength << ' ' << numKnockouts << '\n' ;
        }

        Boxer( exemplar e ) : Guy(e) {};
	    Boxer() = default ;

    private:
        static Boxer prototype ;
        virtual std::string class_tag() const override { return "Boxer" ; }
        virtual Person* do_create( std::istream& stm ) override {
            auto p = new Boxer ;
            p->load(stm) ;
            return p ;
        }
};

Boxer Boxer::prototype{ exemplar{} } ;

struct Cheerleader : Girl {
	int cheeringLoudness, batonSkill;
	Cheerleader (const std::string& name, int a, int b, int c, int d) : Girl(name, a, b),
		cheeringLoudness(c), batonSkill(d) {}

	virtual void load( std::istream& stm ) override {
	    Girl::load(stm) ;
	    stm >> cheeringLoudness >> batonSkill >> std::skipws ;
	}
	virtual void save( std::ostream& stm ) const { stm << class_tag() << '\n' ; do_save_(stm) ;}

	protected:
	    virtual void do_save_( std::ostream& stm ) const override {
            Girl::do_save_(stm) ;
            stm << cheeringLoudness << ' ' << batonSkill << '\n' ;
        }

        Cheerleader( exemplar e ) : Girl(e) {};
	    Cheerleader() = default ;

	private:
        static Cheerleader prototype ;
        virtual std::string class_tag() const override { return "Cheerleader" ; }
        virtual Person* do_create( std::istream& stm ) override {
            auto p = new Cheerleader ;
            p->load(stm) ;
            return p ;
        }
};

Cheerleader Cheerleader::prototype{ exemplar{} } ;

int main() {
    const char* const path = "people.txt" ;

    {
        std::list<Person*> people { new Guy( "John Doe", 5 ),
                                     new Cheerleader( "Mary May", 1, 2, 3, 4 ),
                                     new Boxer( "Rocky Balboa", 7, 8, 9 ),
                                     new Girl( "Madame Yes", 10, 11 ) } ;
        std::ofstream file(path) ;
        for( auto p : people ) { p->save(file) ; file << '\n' ; delete p ; }
    }
    // std::cout << "----- file contains: -------\n" << std::ifstream(path).rdbuf() ;
    {
        std::list< std::unique_ptr<Person> > people ;
        std::ifstream file( "people.txt" ) ;

        while(true) {
             auto p = Person::create(file) ;
             if(p) people.push_back( std::move(p) ) ;
             else break ;
        }

        // std::cout << "----- created from file: -------\n" ;
        for( auto& p : people ) p->save( std::cout << '\n' ) ;
    }
}

http://coliru.stacked-crooked.com/a/13f27f1c15bb7e3c
Topic archived. No new replies allowed.