How file reading should be done?

Hi, folks! It´s me here.

After dealing with fstream for a while, I decided to put this topic up to ask "How file reading should be done?".

This topic could be general purpose, because to my knowledge, many people may have trouble with fstream & file reading in general.

So, here´s the deal: I would like to have definition file (file format could be .def for example). There, I´d store names of the levels of my game, like following:

1
2
3
4
5
6
7
world Main1
    level Test1 file main1_test1.map
    level Test2 file main1_test2.map
world Main2
    level Test1 file main2_test1.map
    level Test2 file main2_test2.map
    level Test3 file main2_test3.map


And if possible, I´d store my game characters into this file, too:

1
2
3
4
5
6
7
8
9
char player Bob
    armor true
    armor 100
    health 100
    lives 5
char enemy Robocop
    armor true
    armor 200
    health 1000


You possibly get the idea.

What could be way smart enough to read information in this kind of form?
When you say the format would be .def, do you mean it will be a binary file containing the data in a hierachical way, as shown by your diagrams? Or do you mean a text file?

For your purposes, I would prob. use a text file (whatever ext you decide to use). This will make it a lot easier to test your game as you will be able to manually edit the configuration file.

If you're doing this as an exercise, I would design the format to be easy to parse and validate.

If the file format is an indidental part of your task, you might want to look at the various standard serialization formats, such as YAML, JSON and XML (see Wikipedia, etc. for info). Writing a parser to handle a subset of YAML, for example, should be straightforward. And your examples already look a bit YAML like.

Andy

P.S. In "real life" you would generally use an existing library.
P.P.S. In the Windows world, .def has other uses; .conf and .config are commonly used for configuration files.
Personally I would probably write insertion and extraction operators for my characters. These are "friend functions" of the class that holds the data that you want to read and write:
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
#include <string>
#include <vector>
#include <fstream>
#include <iostream>

class Character
{
	std::string type;
	std::string name;

	size_t armour;
	size_t health;
	size_t lives;

public:

	// Insertion operator: write data out
	friend std::ostream& operator<<(std::ostream& os, const Character& c)
	{
		os << c.type << '\n';
		os << c.name << '\n';
		os << c.armour << '\n';
		os << c.health << '\n';
		os << c.lives << '\n';
		return os;
	}

	// Extraction operator: Read data in
	friend std::istream& operator>>(std::istream& is, Character& c)
	{
		// use std::getline() because type & name may contain spaces
		std::getline(is, c.type);
		std::getline(is, c.name);
		is >> c.armour;
		is >> c.health;
		is >> c.lives;
		is >> std::ws; // skip end-of-line char
		return is;
	}
};

int main()
{
	// Declare a vector to hold all the characters
	std::vector<Character> cast;

	// Open an input file
	std::ifstream ifs("data/characters.txt");

	// Read Characters data in from file
	Character c;
	while(ifs >> c) cast.push_back(c);

	// Display characters to console to
	// test if we read them in correctly
	for(size_t i = 0; i < cast.size(); ++i)
	{
		std::cout << cast[i] << '\n';
	}
}
Input File:
Player
Bob
100
100
5
Non Player
Jos the Inn Keeper
0
100
1
Monster
Robocop
200
1000
1

I personally would not bother to label the data inside the file because that just makes reading it in and out more complicated. The computer doesn't need the data to be labelled because it always reads the data in and out in the same order.
When you say the format would be .def, do you mean it will be a binary file containing the data in a hierachical way, as shown by your diagrams? Or do you mean a text file?


As long as the file is well readable that´s all that matters. I was also thinking about the situation of using scripting languages, like Lua or Python, to read external files in runtime. I´ve heard scripting is "the thing" today to make software easier to maintain. I didn´t mention about runtime file reading before, because I´m still trying to learn how to do file reading in a good way.

Personally I would probably write insertion and extraction operators for my characters. These are "friend functions" of the class that holds the data that you want to read and write:


The code looks promising, I think I´d better familiarize myself with it. I haven´t dealt with friend -keyword that much, because I´ve heard it breaks encapsulation & it should be used only if essential. I guess this time it is necessary, because std::istream & std::ostream have private data required to deal with, right? Please correct me, if I´m wrong.

Thanks andywestken & Galik for your help. I hope other people have something to suggest or have some way to help me/us yet.
The reason that the insert and extraction operator functions need to be declared friends is because they have to access the class to output or to initialize all of its internal data, including private and protected members.

The friend keyword should only really be used when you have two classes or a class and a function that completely depend on each other. In this case the extraction operator functions are part of how class Character operates. Although the functions are not technically part of the class they are logically connected to it because they provide part of its functionality.

So, in this case, I think you could argue that encapsulation is not being broken because the extraction operators are simply a part of the class that needs to be defined separate from it.

The friend keyword is easily misused and so you are right to be wary of it. However the usage I provided here is very much the "C++" way or extending classes to enable them to be inserted into and extracted from streams (like file streams).

Although I have written the insert and extraction operators inside the definition of your class, they are logically separate from it, existing at global scope. Here is the same class with their definitions written separately:
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
#include <string>
#include <vector>
#include <fstream>
#include <iostream>

class Character
{
	std::string type;
	std::string name;

	size_t armour;
	size_t health;
	size_t lives;

public:

	// They still need to be 'declared' friends inside the class definition

	friend std::ostream& operator<<(std::ostream& os, const Character& c);
	friend std::istream& operator>>(std::istream& is, Character& c);
};

// They are defined externally from the class, although being 'logically' a part of it.

// Insertion operator: write data out
std::ostream& operator<<(std::ostream& os, const Character& c)
{
	os << c.type << '\n';
	os << c.name << '\n';
	os << c.armour << '\n';
	os << c.health << '\n';
	os << c.lives << '\n';
	return os;
}

// Extraction operator: Read data in
std::istream& operator>>(std::istream& is, Character& c)
{
	// use std::getline() because type & name may contain spaces
	std::getline(is, c.type);
	std::getline(is, c.name);
	is >> c.armour;
	is >> c.health;
	is >> c.lives;
	is >> std::ws; // skip end-of-line char
	return is;
}

int main()
{
	// Declare a vestor of characters
	std::vector<Character> cast;

	std::ifstream ifs("data/characters.txt");

	// Read characters in from file
	Character c;
	while(ifs >> c) cast.push_back(c);

	// Display characters to console to
	// test if we read them in correctly
	for(size_t i = 0; i < cast.size(); ++i)
	{
		std::cout << cast[i] << '\n';
	}
}
You could also use parser like boost::spirit or flex/bison
Last edited on
I would label the data. It might make life harder for the computer, and take longer to code, but it make life easier for the person who's editing the file.

For my unit test configuration files I usually use one line per entry with name-value pairs. As I have a little reusable name-value-pair reader class, with its own unit test, it takes me no time to add config file support to yet another new unit test or other application.

If you don't label the entries then it's up to the programmer or tester to know that (e.g.) column 3 is the number of lives. If a lot of values are numeric, life is even harder. Esp. as the number of supported traits increases.

Using named values also supports sparse data files. You can store only those values which have been changed from the default value.

Andy
Last edited on
Topic archived. No new replies allowed.