2

I'm implementing a thing that generates results and writes them to a file of certain format.

Fairly simple, but I want this to be dynamic.

I'll throw down a few classes.

Data - base class for all results
DataFile - base class for all file format types, has method addData(Data * data)

ErrorData - derived from Data, contains data about an error.
InfoData - derived from Data, contains generic information.

XmlFile - derived from DataFile, contains Data in XML format.
BinaryFile - derived from DataFile, contains data in binary format.

My question is this:

Where do i put the implementation on how to write ErrorData into an XmlFile?

What I don't like to see in the answer:

  1. A new member function to Data, DataFile, ErrorData or XmlFile (because this means that i need to add those each time I add a new Data or DataFile derived class)
  2. Type casting to derived types.
  3. General ugliness :)

I know my basic C++ virtualization and things, no need to be ultra specific.

I do appreciate a few code pieces though, they leave less ambiguity.

I had some thoughts on making a DataWriter base class and derive classes from that which would know how to write Data of certain type to a DataFile of certain type, but I'm a bit uncertain of the specifics of that.

EDIT:

I'll clarify a bit more in the form of an example.

Let's have 2 new file formats, FormatATxtFile and FormatBTxtFile.

Let's assume that we have an InfoData object and it has the parameters:

Line number of the message : 34
Message content : Hello World

The object, written into FormatATxtFile, looks like this in the file:

Line:34;Txt:Hello World;Type:Info

And in FormatBTxtFile it would look something like this:

@Info,34,Hello World

A sort of way to export data into a different format. I don't need Importing, at least now.

What the code using this would look like:

DataFile * file = DataFileFactory::createFile(type);

std::vector<Data*> data = generateData();

file->setData(data);
file->writeTo("./FileName"); // correct end is added by DataFile type, ie .txt

Edit:

It seems that I didn't make clear enough what problems arise with Xml and binary file formats. I'm sorry.

Let's use the same InfoData object as above and push it to the XmlFile format. It might produce something like this, under a certain element:

<InfoLog>
    <Info Line="34">Hello World</Info>
</InfoLog>

Let's assume that the ErrorData class would have these parameters:

Line number of the error : 56
Error text : LINK : fatal error LNK1168
10 Lines prior to error message: text1...
10 Lines after error message: text2...
Entire log af what happened: text3...

Now when this is pushed into an XML format, it would need to be something totally different.

<Problems>
    <Error>
        <TextBefore>text1...</TextBefore>
        <Text line = 56>
            LINK : fatal error LNK1168
        </Text>
        <TextAfter>text1...</TextAfter>
    </Error>
    ...
</Problems>

The entire file might look something like this:

<Operation>
    <InfoLog>
        <Info Line="34">Hello World</Info>
        <Info Line="96">Goodbye cruel World</Info>
    </InfoLog>
    <Problems>
        <Error>
            <TextBefore>text1...</TextBefore>
            <Text line = 56>
                LINK : fatal error LNK1168
            </Text>
            <TextAfter>text1...</TextAfter>
        </Error>
        <Error>
            <TextBefore>sometext</TextBefore>
            <Text line = 59>
                Out of cheese error
            </Text>
            <TextAfter>moretext</TextAfter>
        </Error>
    </Problems>
</Operation>
0xbaadf00d
  • 2,535
  • 2
  • 24
  • 46
  • Some ideas here: http://stackoverflow.com/questions/1809670/how-to-implement-serialization-in-c – sje397 May 06 '11 at 06:42

4 Answers4

2

Instead of trying to find a place to put this inside a class, what about a new function?

void copyData(const ErrorData *data, DataFile *output)
{
    // ...
}

You can then overload this function for whatever data types you want to convert.

Alternately, you could perhaps use a template:

template<typename A, typename B> copyData(const A *data, const B *output);

Then you can specialise the template for specific types you need to support.

Greg Hewgill
  • 951,095
  • 183
  • 1,149
  • 1,285
  • +1: Free functions are the way to go in C++. If you want to make that solution really beautiful (in my opinion), you replace `DataFile` by a simple `std::ostream`. – Björn Pollex May 06 '11 at 06:44
  • @Space: Well, they're are not the only way to go. Especially if you want genericity. I'm so glad C++ is multi-paradigm. :) – Xeo May 06 '11 at 06:47
  • For this, I do need the derived pointer, hence type-casting to derived classes from Data or DataFile? Or have I misunderstood something? – 0xbaadf00d May 06 '11 at 07:51
  • No typecasting is necessary. For example, using the overload solution, just write a new function called `copyData(const ErrorData *data, XmlFile *output)` or whatever and write the specific implementation there. You can have as many of these as you need, all named `copyData`. – Greg Hewgill May 06 '11 at 07:54
2

Do like the standard library does - go with virtual functions / operators. We all can use istream& and extract what we want from it with operator>>, while we totally don't care for the underlying stream, be it cin, fstream or a stringstream. And rather take your data by reference then (Data& data).

Xeo
  • 129,499
  • 52
  • 291
  • 397
  • Actually, the C++ standard library usually prefers non-member functions over member-functions (most of the stream extraction insertion operators are free functions). – Björn Pollex May 06 '11 at 06:48
  • Well, internally those `operator>>` still use virtual functions, so no harm there. – Xeo May 06 '11 at 06:49
  • Xeo: looking at GCC 4.5.2, I can't see any use of virtual functions by `operator>>`... can you be more specific about which compiler you're talking about, and where/how it uses virtual dispatch? – Tony Delroy May 06 '11 at 07:44
  • I guess i could create a stream operator and a virtual function for the base class to provide the data to write. Very neat and nice, but doesn't solve the problem that I need to write data that looks totally different in different formats. I didn't make this clear enough in the question, i'll update. – 0xbaadf00d May 06 '11 at 07:48
  • @just: I really wanted to provide some code which does exactly as your example, with virtual functions and some inheritence stuff, but after a while I found it just too tedious. Maybe you really need to overload the functions, or do some trickery with Boost.Fusion to map a type to a function to call or something ([ideas here](http://stackoverflow.com/questions/5904680/can-i-gain-access-to-a-component-by-type/5905102#5905102)). In general, another question remains. Is the `InfoData` always formatted the same way? – Xeo May 06 '11 at 08:35
  • That depends on the file format. On one format it might be static, but on another it might change depending on the content. You don't need to write code to do what I'v shown, I know my way around cpp. The Model to solve this problem is what i'm after. Here's an example on how to do the "virtualization" http://www.velocityreviews.com/forums/t282211-how-to-make-operator-a-virtual-function.html – 0xbaadf00d May 06 '11 at 11:07
  • @just: No, I just wanted to write the code because I like writing code. :) – Xeo May 06 '11 at 11:45
1

If you consider the code below, it presents a minimal illustration of how to arbitrarily combine generic field access with generic field streaming - factoring your various requirements. If the applicability or utility's not clear, do let me know....

#include <iostream>
#include <string>

struct X
{
    int i;
    double d;

    template <typename Visitor>
    void visit(Visitor& visitor)
    {
        visitor(i, "i");
        visitor(d, "d");
    }
};

struct XML
{
    XML(std::ostream& os) : os_(os) { }

    template <typename T>
    void operator()(const T& x, const char name[]) const
    {
        os_ << '<' << name << '>' << x << "</" << name << ">\n";
    }

    std::ostream& os_;
};

struct Delimiter
{
    Delimiter(std::ostream& os,
              const std::string& kvs = "=", const std::string& fs = "|")
      : os_(os), kvs_(kvs), fs_(fs)
    { }

    template <typename T>
    void operator()(const T& x, const char name[]) const
    {
        os_ << name << kvs_ << x << fs_;
    }

    std::ostream& os_;
    std::string kvs_, fs_;
};

int main()
{
    X x;
    x.i = 42;
    x.d = 3.14;

    XML xml(std::cout);
    Delimiter delimiter(std::cout);

    x.visit(xml);
    x.visit(delimiter);
}
Tony Delroy
  • 102,968
  • 15
  • 177
  • 252
  • Nice clean solution. By virtualizing Visit, I can provide different values from different derived classes. I'll give this approach some though, maybe i can somehow fit it into what i need. – 0xbaadf00d May 06 '11 at 08:19
1

I've been playing a little with your question, and that's what I've come up:

#include <iostream>
#include <list>
#include <map>
#include <string>

using namespace std;

class DataFile;

class Data {
public:
    virtual void serializeTo(DataFile*) = 0;
};

class DataFile {
public:
    void addData(Data* d) {
        _data.push_back(d);
    }
    virtual void accept(string paramName, string paramValue) {
        _map[paramName] = paramValue;
    }

    virtual void writeTo(string const& filename) = 0;

protected:
    list<Data*> _data;
    map<string, string> _map;
};

class FormatATxtFile: public DataFile {
public:
    void writeTo(string const& filename) {
        cout << "writing to " << filename << ".txt:" << endl;
        for(list<Data*>::iterator it = _data.begin(); it != _data.end(); ++it) {
            (*it)->serializeTo(this);       

            cout << "Line:" << _map["Line"] << "; "
                << "Txt:" << _map["Txt"] << "; "
                << "Type: " << _map["Type"]
                << endl;
        }
        cout << endl;
    }
};

class FormatBTxtFile: public DataFile {
public:
    void writeTo(string const& filename) {
        cout << "writing to " << filename << ".b-txt" << endl;
        for(list<Data*>::iterator it = _data.begin(); it != _data.end(); ++it) {
            (*it)->serializeTo(this);

            cout << "@" << _map["Type"] << "," << _map["Line"] << "," << _map["Txt"]
                << endl;
        }
        cout << endl;
    }
};

class InfoData: public Data {
    public:
        void serializeTo(DataFile* storage) {
            storage->accept("Line", line);
            storage->accept("Txt", txt);
            storage->accept("Type", "Info");
        }
        string line;
        string txt;
    };

int main()
{
    FormatATxtFile fileA;
    FormatBTxtFile fileB;
    InfoData info34;
    info34.line = "34";
    info34.txt = "Hello World";
    InfoData info39;
    info39.line = "39";
    info39.txt = "Goodbye cruel World";
    fileA.addData(&info34);
    fileA.addData(&info39);
    fileB.addData(&info34);
    fileB.addData(&info39);
    fileA.writeTo("./Filename");
    fileB.writeTo("./Filename");    
}

actually, it doesn't really write to file but it's easy to change to suit your needs.

The output of this sample code is:

writing to ./Filename.txt: Line:34;
Txt:Hello World; Type: Info Line:39;
Txt:Goodbye cruel World; Type: Info

writing to ./Filename.b-txt
@Info,34,Hello World
@Info,39,Goodbye cruel World

As you see, Data need to provide the DataFile with parameters identified by a name and a value, and each specialization of DataFile will handle it the way it likes.

HTH

Simone
  • 11,655
  • 1
  • 30
  • 43