More Advanced, maybe not Class problem binary file mode

Started by
10 comments, last by justinwalsh 20 years, 5 months ago
I am writing a Class that needs to store its own data to a config file. I will give you the code below that works and reads the data just fine, in one fasion... Hear is my class first... the functions readInConfigFile, and writeOutConfigFile give me the troubles... but ignore those functions for now... ConfigFileWriter.h

#ifndef CONFIGFILEWRITER_H
#define CONFIGFILEWRITER_H

class GameSettings  {
	public:
		GameSettings(int CwindowWidth, int CwindowHeight, int CwindowXpos, 
			int CwindowYpos, int CwindowBitDepth, bool CwindowFullscreen)
			:windowWidth(CwindowWidth),windowHeight(CwindowHeight),
			windowXPos(CwindowXpos),windowYPos(CwindowYpos),
			windowBitDepth(CwindowBitDepth),windowFullscreen(CwindowFullscreen){}
		GameSettings(void);
		~GameSettings(){}

		//Data Reading Functions
		int getWindowWidth()const {return windowWidth;}
		int getWindowHeight()const {return windowHeight;}
		int getWindowX()const {return windowXPos;}
		int getWindowY()const {return windowYPos;}
		int getWindowBitDepth()const {return windowBitDepth;}
		bool getWindowFullscreen()const {return windowFullscreen;}
		bool readInConfigFile(char filename[20]);

		//Data Storing Functions
		void setWindow(int width, int height, int x, int y, int depth, bool fullscreen);
		void setWindowSize(int width, int height) {windowWidth = width; windowHeight = height;}
		void setWindowPos(int x, int y) {windowXPos = x; windowYPos = y;}
		void setWindowBitDepth(int BD) {windowBitDepth = BD;}
		void setWindowFullscreen(bool set)  {windowFullscreen = set;}
		bool writeOutConfigFile(char filename[20]);


	private:
		int windowWidth;
		int windowHeight;
		int windowXPos;
		int windowYPos;
		int windowBitDepth;
		bool windowFullscreen;
};

#endif
ConfigFileWriter.cpp

#include "ConfigFileWriter.h"
#include <fstream.h>

GameSettings :: GameSettings(void)  {
	windowWidth = 640;
	windowHeight = 480;
	windowXPos = 100;
	windowYPos = 100;
	windowBitDepth = 16;
	windowFullscreen = false;
}

bool GameSettings::readInConfigFile(char filename[20])  {
	ifstream fin(filename, ios::binary);
	if(!fin)
		return false;

	fin.read((char *) this, sizeof this);
	fin.close();
	return true;
}

void GameSettings::setWindow(int width, int height, int x, int y, int depth, bool fullscreen)  {
	windowWidth = width;
	windowHeight = height;
	windowXPos = x;
	windowYPos = y;
	windowBitDepth = depth;
	windowFullscreen = fullscreen;
}

bool GameSettings::writeOutConfigFile(char filename[20])  {
	ofstream fout(filename, ios::binary);
	if (!fout)
		return false;

	fout.write((char*) this, sizeof this);
	fout.close();
	return true;
}
Ok now you have the class so hear is a working implementation of writing the class to a file... WORKING VERSION:

#include "ConfigFileWriter.h"
#include <fstream.h>

int main(int argc, char* argv[])
{	
	ofstream fout("config.cfg", ios::binary);
	if (!fout)  {
		cout << "Unable to open file\n";
		return(1);
	}

	GameSettings Temp(640,480,10,10,16,false);
	//Temp.writeOutConfigFile("config.cfg");
	fout.write((char*) &Temp,sizeof Temp);
	fout.close();

	cout << "Wrote Class Temp to file\n";

	ifstream fin("config.cfg", ios::binary);
	if(!fin)  {
		cout << "Unable to open file\n";
		return(1);
	}

	GameSettings Temp2(100,200,300,400,500,false);

	cout << "Temp2 windowHeight = " << Temp2.getWindowHeight() << endl;
	cout << "Temp2 windowWidth = " << Temp2.getWindowWidth() << endl;

	//Temp2.readInConfigFile("config.cfg");   
	fin.read((char *) &Temp2, sizeof Temp2);
	fin.close();
	cout << "Read Class Temp from file\n";

	cout << "Temp2 windowHeight = " << Temp2.getWindowHeight() << endl;
	cout << "Temp2 windowWidth = " << Temp2.getWindowWidth() << endl;

	return 0;
}
now what i want to do, is have the class know how to write and read itself hence the functions readInCOnfigFile but it dsnt work, not sure how to make it work, do i need copy constructors and define a new class? or is there an easier way? hear is what i am trying to do....

#include "ConfigFileWriter.h"
#include <fstream.h>

int main(int argc, char* argv[])
{	
	//ofstream fout("config.cfg", ios::binary);
	//if (!fout)  {
	//	cout << "Unable to open file\n";
	//	return(1);
	//}

	GameSettings Temp(640,480,10,10,16,false);
	Temp.writeOutConfigFile("config.cfg");
	//fout.write((char*) &Temp,sizeof Temp);
	//fout.close();

	cout << "Wrote Class Temp to file\n";

	//ifstream fin("config.cfg", ios::binary);
	//if(!fin)  {
	//	cout << "Unable to open file\n";
	//	return(1);
	//}

	GameSettings Temp2(100,200,300,400,500,false);

	cout << "Temp2 windowHeight = " << Temp2.getWindowHeight() << endl;
	cout << "Temp2 windowWidth = " << Temp2.getWindowWidth() << endl;

	Temp2.readInConfigFile("config.cfg");   
	//fin.read((char *) &Temp2, sizeof Temp2);
	//fin.close();
	cout << "Read Class Temp from file\n";


	cout << "Temp2 windowHeight = " << Temp2.getWindowHeight() << endl;
	cout << "Temp2 windowWidth = " << Temp2.getWindowWidth() << endl;

	return 0;
}
please help me out, i dont understand why it dsnt work? i think the this is the problem.
Advertisement
bad. Best method is to use serialization.

basically your "write out" function needs to output each variable individually.

And might I recommend a text format? human readable is very nice.

ofstream f("configfile");f << "width:" << width << endl;f << "height:" << height << endl;ifstream f("configfile");string t;while(getline(f,t,'':'')) {  switch(hash(t)) {  case hash("width"):    f >> width;    break;  case hash("height"):    f >> height;    break;  }} 


is a general idea (untested)
would you like to explain why its bad? I know it will work, and its less syntax then the way you are showing me, and the only person that needs to read my congig file is the program itself, and no one else, so please enlighten me as to why my idea is bad.


If anyone knows how to make this work without defining a class within a class and then copying the values back. There must be a way for a class to write over itself right?
would you like to explain why its bad?

What do you think will happen with classes that contain pointers to some other data ? Or contain objects with contructors ? Or virtual functions ? Or immutable members (constants, references) ?

The reason why some classes have copy constructors (and assignment operators) is exactly the same reason why a bitwise copy is not always possible.

There must be a way for a class to write over itself right ?

An object can only be constructed once (unless you explicitely destroy it and do a placement new).

Member by member assignment is the prefered method. Yes, it takes "more syntax", get over it. There ain''t no such thing as a free lunch.

Another solution is to write a constructor that takes an std::istream& parameter and use it to read the data for your object.


[ Start Here ! | How To Ask Smart Questions | Recommended C++ Books | C++ FAQ Lite | Function Ptrs | CppTips Archive ]
[ Header Files | File Format Docs | LNK2001 | C++ STL Doc | STLPort | Free C++ IDE | Boost C++ Lib | MSVC6 Lib Fixes ]
"Debugging is twice as hard as writing the code in the first place. Therefore, if you write the code as cleverly as possible, you are, by definition, not smart enough to debug it." — Brian W. Kernighan
i would just like to say, i had found a way to get it to work. I just created a copy constructor and inside the read and write functions make a new Temp class set its values and store it and read it the same way. It works just like i need it to.

The only thing i care about is the data within the class being saved, and restored. This one class will not use refrences or pointers etc, i am useing it as a means of storage, and think it should be able to know how to store and retrieve itself.

There is still no answer as to why doing it the way that was given as an example, serialization, is better then the way i am trying to illistrate.


thanks for your insight.

[edited by - justinwalsh on November 13, 2003 4:01:40 AM]
If you''re just writing this for yourself, then it doesn''t matter at all how you do it because no one else will be affected. If you are making something to be distributed, then that is a bad way of doing it because there are too many ways for it to break. You cannot be sure of how the compiler will format the data in your class. It may decide to add padding to the end. It may even decide to add padding somewhere in the middle. This could change depending on the compile flags that you use. This means that the config file may not remain valid across compilations. It will very likely not remain valid if you ever try to compile on a different machine.

Such a simple file format is also inadequate for anything except the simplest program. If you need to add a new field to the config file, all of your old config files will no longer work. This may be acceptable during the developement process, but unacceptable if released as an upgrade. Naming fields in the config file and adding version information can help this. A human readable format is nice so that if something goes wrong with the file, you stand a good chance of finding out why.
quote:Original post by justinwalsh
There is still no answer as to why doing it the way that was given as an example, serialization, is better then the way i am trying to illistrate.


Serialization is the name of the process of reading/writing objects to a stream. Your own technique is a form of serialisation, but which is only valid for C-style structs (a.k.a. C++ POD-types).

One reason why your solution is bad is that it is very brittle.

If you modify the internal structure of your class, by adding/removing/modifying/rearranging members, your previously serialized data becomes garbage. Furthermore, it is limited with respect to the kind of data that can be serialized. It also assumes that all the data members in the class have to be saved, are all available for read/write access and are *all* that the class contain. i.e. no compiler-generated ancillary data - there''s a reason why constructors are ''special functions'', right ?

In addition to being modifiable via a text editor, keyword-based serialization, as proposed by C-Junkie (even though his code is a first-class abomination), offers more flexibility. If you add new members to your class, you can usually provide sensible default for those members which had not been previously serialized. If your remove member, those entries which have no corresponding data members can be ignored. Though, I''ll admit that kind of machinery is not always appropriate.

Yet, an important benefit of explicit member-by-member serialization is that you can recursively serialize complex objects by calling the serialization functions of the appropriate data members (in C-Junkie''s example, by overloading operator<< and operator>> for the members''s types).

So, take the following example, using formatted I/O.

class Foo{   int x,y,z;   friend std::istream& operator>>( std::istream&, Foo& );   friend std::ostream& operator<<( std::ostream&, const Foo& );};class Bar{   Foo a,b;   std::string c;   int d,e,f;   friend std::istream& operator>>( std::istream&, Bar& );   friend std::ostream& operator<<( std::ostream&, const Bar& );}std::ostream& operator<<( std::ostream& os, const Foo& f ){   os << f.x << '' '' << f.y << '' '' << f.z;   return os;}std::istream& operator>>( std::istream& is, Foo& f ){   is >> f.x >> f.y >> f.z;   return is;}std::ostream& operator<<( std::ostream& os, const Bar& b ){   // We''re not saving b.d;   os << b.a << ''\n''      << b.b << ''\n''      << b.c << ''\n''        // Save string on a separate line      << b.e << ''\n''      << b.f << std::endl;   return os;}std::istream& operator>>( std::istream& is, Bar& b ){   is >> b.a >> b.b;   std::getline(is,b.c);    // Read string off the whole line   is >> b.c >> b.e >> b.f;   b.d = 42; // Using a default value;   return is;}


Note how Foo objects are automatically written and read back correctly. Of course, if you want unformatted (i.e. binary) I/O, it''s possible too.

class Foo{   int x,y,z;public:   std::istream& Load( std::istream& );   std::ostream& Save( std::ostream& ) const;};class Bar{   Foo a,b;   std::string c;   int d,e,f;public:   std::istream& Load( std::istream& );   std::ostream& Save( std::ostream& ) const;}std::ostream& Foo::Save( std::ostream& os ) const{   os.write( (const char*)&x, sizeof(x) );   os.write( (const char*)&y, sizeof(y) );   os.write( (const char*)&z, sizeof(z) );   return os;}std::istream& Foo::Load( std::istream& is ){   is.read( (char*)&x, sizeof(x) );   is.read( (char*)&y, sizeof(y) );   is.read( (char*)&z, sizeof(z) );   return is;}std::ostream& Bar::Save( std::ostream& os ) const{   a.Save(os);   b.Save(os);   int size = c.size();   os.write( (const char*)&size, sizeof(size) );   os.write( c.data(), size );   // Still not saving d   os.write( (char*)&e, sizeof(e) );   os.write( (char*)&f, sizeof(f) );   return os;}std::istream& Bar::Load( std::istream& is ){   a.Load(is);   b.Load(is);   // Load an arbitrary-length string   int size;   is.read( (char*)&size, sizeof(size) );   char* buffer = new char[size]   is.read( buffer, size );   c.assign( buffer, buffer+size);   delete[] buffer;   d = 42;   is.read( (char*)&e, sizeof(e) );   is.read( (char*)&f, sizeof(f) );   return is;}


Or with a bit of template magic

template<class T> std::ostream& Save( std::ostream& os, const T& t ){  return t.Save(os);}template<>std::ostream& Save<int>( std::ostream& os, const int& t ){   os.write( (const char*)&t, sizeof(t) );   return os;}template<>std::ostream& Save<std::string>( std::ostream& os,                                  const std::string& t ){   int size = c.size();   Save(os, size);   os.write( c.data(), size );   return os;}std::istream& Load( std::istream& is, T& t ){  return t.Load(is);}template<>std::istream& Load<int>( std::istream& is, int& t ){   is.read( (char*)&t, sizeof(t) );   return is;}template<>std::istream& Load<std::string>( std::istream& is,                                  std::string& t ){   int size;   Load(is, size);   char* buffer = new char[size]   is.read( buffer, size );   t.assign( buffer, buffer+size);   delete[] buffer;   return is;}std::ostream& Foo::Save( std::ostream& os ) const{   Save(os, x);   Save(os, y);   Save(os, z);   return os;}std::istream& Foo::Load( std::istream& is ){   Load(is, x);   Load(is, y);   Load(is, z);   return is;}std::ostream& Bar::Save( std::ostream& os ) const{   Save(os,a);   Save(os,b);   Save(os,c);   // Still not saving d   Save(os,e);   Save(os,f);   return os;}std::istream& Bar::Load( std::istream& is ){   Load(is,a);   Load(is,b);   Load(is,c);   d = 42;   Load(is,e);   Load(is,f);   return is;}


There are of course many other ways to implement it, including ways that will actally compile

So, in conclusion, bit blasting is fine for basic types and C structures, but once you get something more complex, saving each member explicitely is the way to go.

[ Start Here ! | How To Ask Smart Questions | Recommended C++ Books | C++ FAQ Lite | Function Ptrs | CppTips Archive ]
[ Header Files | File Format Docs | LNK2001 | C++ STL Doc | STLPort | Free C++ IDE | Boost C++ Lib | MSVC6 Lib Fixes ]
"Debugging is twice as hard as writing the code in the first place. Therefore, if you write the code as cleverly as possible, you are, by definition, not smart enough to debug it." — Brian W. Kernighan
You may want to use a more generic output method:


template<typename type, class traits>std::basic_ostream<type, traits>& operator <<(std::basic_ostream<type, traits>& output_stream, const GameSettings& game){output_stream << game.settingone;...return output_stream;}


This allows for wchar_t support.

[edited by - Jingo on November 13, 2003 5:49:58 AM]
OK, thanks for all your valid answers, I may no reconsider how i am saving my config file. The main reason i first picked this method was that you couldn''t read the file in a text editor, i dont want people to go into the .cfg files and try and change things. One downfall i did notice about my code is that if someone does go into the text editor and change code I have no way of telling that the info is not valid, and will have random data being read back into my class.
The main reason i first picked this method was that you couldn''t read the file in a text editor, i dont want people to go into the .cfg files and try and change things.

People have edited binary files for about as long as games have used them. Security through obscurity = bad.

One downfall i did notice about my code is that if someone does go into the text editor and change code I have no way of telling that the info is not valid, and will have random data being read back into my class.

Checksums.



[ Start Here ! | How To Ask Smart Questions | Recommended C++ Books | C++ FAQ Lite | Function Ptrs | CppTips Archive ]
[ Header Files | File Format Docs | LNK2001 | C++ STL Doc | STLPort | Free C++ IDE | Boost C++ Lib | MSVC6 Lib Fixes ]
"Debugging is twice as hard as writing the code in the first place. Therefore, if you write the code as cleverly as possible, you are, by definition, not smart enough to debug it." — Brian W. Kernighan

This topic is closed to new replies.

Advertisement