We are going to implement a helper class that solves the problem of redirecting a stream and reverting that redirection again with constructor/destructor magic. And then we see how we can put it to use:
- We only need the headers for input, output, and file streams this time. And we declare the std namespace as a default namespace for lookup:
#include <iostream>
#include <fstream>
using namespace std;
- We implement a class, which holds a file stream object and a pointer to a stream buffer. The cout as a stream object has an internal stream buffer, which we can simply exchange. And while we exchange it, we can save what it was before, so we can undo any change later. We could look its type up in the C++ reference, but we can also use decltype to find out what type cout.rdbuf() returns. This is not generally good practice in all situations, but in this case, it's just a pointer type:
class redirect_cout_region
{
using buftype = decltype(cout.rdbuf());
ofstream ofs;
buftype buf_backup;
- The constructor of our class accepts a filename string as its only parameter. The filename is used to initialize the file stream member, ofs. After initializing it, we can feed it into cout as a new stream buffer. The same function that accepts the new buffer also returns a pointer to the old one, so we can save it in order to restore it later:
public:
explicit
redirect_cout_region (const string &filename)
: ofs{filename},
buf_backup{cout.rdbuf(ofs.rdbuf())}
{}
- The default constructor does the same as the other constructor. The difference is, that it does not open any file. Feeding a default-constructed file stream buffer into the cout stream buffer leads to cout being kind of deactivated. It will just drop its input we give it for printing. This can also be useful in some situations:
redirect_cout_region()
: ofs{},
buf_backup{cout.rdbuf(ofs.rdbuf())}
{}
- The destructor just restores our change. When an object of this class runs out of scope, the stream buffer of cout is the old one again:
~redirect_cout_region() {
cout.rdbuf(buf_backup);
}
};
- Let's mock an output-heavy function, so we can play with it later:
void my_output_heavy_function()
{
cout << "some outputn";
cout << "this function does really heavy workn";
cout << "... and lots of it...n";
// ...
}
- In the main function, we first produce some completely normal output:
int main()
{
cout << "Readable from normal stdoutn";
- Now we're opening another scope, and the first thing we do in this scope is instantiating our new class with a text file parameter. File streams open files in read and write mode by default, so it creates this file for us. Any following output will now be redirected to this file, although we use cout for printing:
{
redirect_cout_region _ {"output.txt"};
cout << "Only visible in output.txtn";
my_output_heavy_function();
}
- After leaving the scope, the file is closed and the output is redirected to the normal standard output again. Let's now open another scope in which we instantiate the same class, but via its default constructor. This way the following printed line of text will not be visible anywhere. It will just be dropped:
{
redirect_cout_region _;
cout << "This output will "
"completely vanishn";
}
- After leaving that scope also, our standard output is resurrected and the last line of text output will be readable in the shell again:
cout << "Readable from normal stdout againn";
}
- Compiling and running the program yields the output as we expected it. Only the very first and the very last lines of output are visible in the shell:
$ ./log_regions
Readable from normal stdout
Readable from normal stdout again
- We can see that a new file, output.txt, has been created and contains the output of the first scope. The output of the second scope vanishes completely:
$ cat output.txt
Only visible in output.txt
some output
this function does really heavy work
... and lots of it...