Introduction
I remember, as a young programmer at university, how impressed I was with the productivity of the UNIX gurus. They would write sophisticated software in just one or two lines of code using strangely named tools like bash and awk. It took a while until I realized that the secret to their productivity was program reuse. Commonly, UNIX programmers wrote programs in a way that made them easily reused, where the primary input was the standard in, and the primary output was the standard out. This kind of program is often called a UNIX filter. The UNIX shells such as bash, and tools like awk, provided various ways for filters to be chained together and piped to and from files.
It has always bothered me that there was no easy way to achieve the same kind of effect from within C++ other than communicating directly with the OS. It seems to make perfect sense that if I have the source code to two C++ programs which behave like UNIX filters, redirecting the output from one to the other should be written as a one liner:
MyProgram1 > MyProgram2;
Using Program Objects
In order to write programs so that they can be reused in other programs, you can write the program as a class derived from the Program
class, i.e.:
#include <iostream>
#include "programs.hpp"
class HelloWorldProgram : public Program {
protected:
virtual void Main() {
cout << "Hello world!\n" << endl;
}
};
The corresponding .cpp file would contain only the following:
#include "hello_world.hpp"
int main() {
HelloWorldProgram().Run();
return 0;
}
This might not look like much at first glance, but now your program can be used within another program as if it were a UNIX filter. Consider the following programs:
#include <iostream.hpp>
#include <cctype.hpp>
#include "programs.hpp"
class UpperCaseProgram : public Program {
protected:
virtual void Main() {
char c;
while (cin.get(c)) cout.put(toupper(c));
}
};
Now we can combine them:
#include "programs.hpp"
#include "upper_case.hpp"
#include "hello_world.hpp"
class UpperCaseHelloWorldProgram : public Program {
protected:
virtual void Main() {
HelloWorldProgram() > UpperCaseProgram();
}
};
Notice that the Program
class overloads the greater-than operator (>
).
Redirecting to and from Stream
Program
objects can also be redirected to and from streams, i.e.:
stringstream s;
HelloWorldProgram() > s;
s > UpperCaseProgram();
Programs and streams can also be chained together in arbitrarily long sequences:
fstream f("c:\\tmp.txt");
stringstream s;
HelloWorldProgram() > f > UpperCaseProgram() > s;
The only caveat is that one stream can't be redirected to another stream.
The Program Class
Here is the code for the Program
class:
class Program {
public:
Program() {
old_in = cin.rdbuf();
old_out = cout.rdbuf();
old_err = cerr.rdbuf();
old_log = clog.rdbuf();
}
~Program() {
ResetStreams();
}
void Run() {
Main();
ResetStreams();
}
void SetStdIn(istream& in) {
cin.rdbuf(in.rdbuf());
}
void SetStdOut(ostream& out) {
cout.rdbuf(out.rdbuf());
}
void SetStdErr(ostream& err) {
cerr.rdbuf(err.rdbuf());
}
void SetStdLog(ostream& log) {
clog.rdbuf(log.rdbuf());
}
void ResetStreams() {
cin.rdbuf(old_in);
cout.rdbuf(old_out);
cerr.rdbuf(old_err);
clog.rdbuf(old_log);
}
protected:
virtual void Main() = 0;
private:
streambuf* old_in;
streambuf* old_out;
streambuf* old_err;
streambuf* old_log;
};
The Redirection Operator
The greater-than operator (>) is used as the redirection operator. The following overloads are provided:
PipeChain operator>(Program& in, Program& out) {
return PipeChain(in, out);
}
PipeChain operator>(iostream& in, Program& out) {
return PipeChain(in, out);
}
PipeChain operator>(Program& in, iostream& out) {
return PipeChain(in, out);
}
PipeChain& operator>(PipeChain& in, Program& out) {
in.ChainTo(out);
return in;
}
PipeChain& operator>(PipeChain& in, iostream& out) {
in.ChainTo(out);
return in;
}
The only really tricky part of the library is how PipeChain
works. A PipeChain
is constructed by the first redirection operator in a chain. In other words, for the example a > b > c;
, a > b
returns a PipeChain
, which is then passed by reference to PipeChain& > c;
. This allows us to construct arbitrarily long PipeChain
s. Only when the chain is destroyed are the programs are redirected and then run.
Anyway, the only thing you really need to know about PipeChain
is that when you pipe from a program to another, the programs are run sequentially. The first program stores its output in a stringstream
, which is then fed as the input to the second program only after the first program is finished running. The reason for this approach is that it is portable to even single threaded environments.
Summary
When writing a program to be reused in the manner described, always keep in mind that the standard input and standard output might be redirected. Also, use only C++ streams and not C-style I/O functions. This library is very exciting and useful to me, because I am continually writing small test and demonstration programs for the various libraries I write. Hopefully, it will be helpful for you as well.