dimanche 28 mai 2017

How to print, scan, binary-write, and binary-read without writing (much) code

I hate writing configuration data classes for my c++ programs: dumb little data classes, with a few standard functions: print() and scan(), sometimes binary read() and write(), and typically a bind() that ties each data member to a boost::program_options::options_description (so I can set configuration data from command-line or config file).

I've made an initial attempt at using templates to reduce the monotony of writing these classes, and I was wondering if someone could tell me if I'm reinventing the wheel before I invest any more time. I'd also like to know if there's anyone at all who appreciates what I'm trying to do (as opposed to thinking me completely nuts.)

The basic idea is that instead of writing print(), scan(), read(), write(), etc, I inherit from Print, Scan, Read, and Write (which I'll call service classes for lack of a better name) which respectively implement for me (via double dispatch) the a fore mentioned methods, each going through the monotonous task of printing, scanning, reading or writing each data member in sequence. Surely, these inherited methods are slower in execution than one written by hand, but I don't really care if it takes a few more microseconds to print or scan configuration data. More importantly, none of these inherited classes add data (so there is no additional overhead for instantiating or copying instances of my config classes).

I have to do 3 things to a class to make it all work:

  1. Provide an implementation for the pure-virtual className() method.
  2. Inherit from the interface classes I want implemented for me.
  3. Depending on which interfaces I choose, I have to include one or two getter methods (one const, and one non-const) for each data member.

Then I also have to write a partial-template specialization of a "Class" class that describes my class. Finally, an instance of each such Class must be loaded to each service class (which use them to figure out how to process class instances).

Below is a main.cpp that hopefully gives you a feel for what I'm doing. Let me know what you think.

#include "Print.hpp"
#include "Scan.hpp"
#include "Write.hpp"
#include "Read.hpp"

using namespace std;

class MyConfig : public Print, public Scan, public Write, public Read {
    public:
        const string className() const { return "MyConfig"; }

        const int&    i() const     { return _i; }
        const string& s() const     { return _s; }

        void i(int value)           { _i = value; }
        void s(const string& value) { _s = value; }

    private:
        int    _i;
        string _s;

        // The Scan and Read interfaces require a way to write the set
        // directly, hence these getter methods that return non-const
        // references.  I make these private, then define the friend below
        // so Scan and Read can get at them.
        //
        int&    i() { return _i; }
        string& s() { return _s; }

        template<class C, class P> friend class Class;
};


// Meta-class describing the members of class MyConfig and
// how to get at them.
//
template<class P>
class Class<MyConfig,P> : public ClassImpl<MyConfig,P> {
    public:
        Class() : ClassImpl<MyConfig,P>("MyConfig") {
            *this->template genMember<int>   ("i", &MyConfig::i, &MyConfig::i);
            *this->template genMember<string>("s", &MyConfig::s, &MyConfig::s);
        }
};

int main(int argc, char** argv) {
    try {
        // Each interface class is loaded with Class objects describing
        // user-data classes that use the interface.
        //
        Print::addClass<MyConfig>();
        Scan ::addClass<MyConfig>();
        Write::addClass<MyConfig>();
        Read ::addClass<MyConfig>();

        MyConfig x;
        x.i(3);
        x.s("abc");

        cout << "Printing config:\n" << x; // print x
        cout << "Scanning config:" << endl;
        cin  >> x; // scan x
        cout << "The scanned config was:" << endl;
        cout << x; // print x again

        cout << "Writing config to file." << endl;
        ofstream ofs;
        ofs.exceptions(ios::failbit | ios::badbit);
        ofs.open("testfile", ios::out | ios::binary);
        x.write(ofs); // write x to file in binary format
        ofs.close();

        cout << "Reading config back from file." << endl;
        MyConfig x2;
        ifstream ifs;
        ifs.exceptions(ios::failbit | ios::badbit | ios::eofbit);
        ifs.open("testfile", ios::in | ios::binary);
        x2.read(ifs); // read x from file in binary format
        ifs.close();
        cout << x2;
    }
    catch(const exception& x) {
        cerr << argv[0] << ":  " << x.what() << endl;
        return 1;
    }
    return 0;
}

Program output:

matt@dworkin:$ ./a.out
Printing config:
i = 3
s = abc
Scanning config:
i=4
s=xyz
The scanned config was:
i = 4
s = xyz
Writing config to file.
Reading config back from file.
i = 4
s = xyz





Aucun commentaire:

Enregistrer un commentaire